first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+266
View File
@@ -0,0 +1,266 @@
Moodle Universal Cache / Cache API
==================================
Sample code snippets
--------------------
A definition:
$definitions = array(
'string' => array( // Required, unique to the component
'mode' => cache_store::MODE_APPLICATION, // Required
'simplekeys' => false, // Optional
'simpledata' => false, // Optional
'requireidentifiers' => array( // Optional
'lang'
),
'requiredataguarantee' => false, // Optional
'requiremultipleidentifiers' => false, // Optional
'requirelockingbeforewrite' => false, // Optional
'requiresearchable' => false, // Optional
'maxsize' => null, // Optional
'overrideclass' => null, // Optional
'overrideclassfile' => null, // Optional
'datasource' => null, // Optional
'datasourcefile' => null, // Optional
'staticacceleration' => false, // Optional
'staticaccelerationsize' => false, // Optional
'ttl' => 0, // Optional
'mappingsonly' => false // Optional
'invalidationevents' => array( // Optional
'contextmarkeddirty'
),
'canuselocalstore' => false // Optional
'sharingoptions' => null // Optional
'defaultsharing' => null // Optional
)
);
Getting 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 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');
Disabling the cache stores.
There are times in code when you will want to disable the cache stores.
While the cache API must still be functional in order for calls to it to work it is possible to disable the use of the cache stores separately so that you can be sure only the cache will function in all circumstances.
// Disable the cache store at the start of your script with:
define('CACHE_DISABLE_STORES', true);
// Disable the cache within your script when you want with:
cache_factory::disable_stores();
// If you disabled it using the above means you can re-enable it with:
cache_factory::reset();
Disabling the cache entirely.
Like above there are times when you want the cache to avoid initialising anything it doesn't absolutely need. Things such as installation and upgrade require this functionality.
When the cache API is disabled it is still functional however special "disabled" classes will be used instead of the regular classes that make the Cache API tick.
These disabled classes do the least work possible and through this means we avoid all manner of intialisation and configuration.
Once disabled it cannot be re-enabled.
// To disable the cache entirely call the following:
define('CACHE_DISABLE_ALL', true);
Cache API parts
---------------
There are several parts that make up the Cache API.
### 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 in the cache API.
In order to get a loader you must use one of two static methods, make or make_from_params.
The loader has been kept as simple as possible, interaction is summarised 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 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 is 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 ad hoc definitions) and must override the appropriate core loader
* The loader handles ttl (time to live) 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.
* There are two reserved store names, base and dummy. These are both used internally.
### 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 ensures 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 its inner workings.
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 ad hoc 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 or request.
The following optional settings can also be defined:
* simplekeys - Set to true if items will always and only have simple keys. Simple keys may contain a-zA-Z0-9_. If set to true we use the keys as they are without hashing them. Good for performance and possible because we know the keys are safe.
* simpledata - Set to true if you know that you will only be storing scalar values or arrays of scalar values. Avoids costly investigation of data types.
* 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.
* requirelockingbeforewrite - If set to true the system will throw an error if you write to a cache without having a lock on the relevant key.
* 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.
* 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.
* staticacceleration - Any data passing through the cache will be held onto to make subsequent requests for it faster.
* staticaccelerationsize - If set to an int this will be the maximum number of items stored in the static acceleration array.
* 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.
* sharingoptions - The sum of the possible sharing options that are applicable to the definition. An advanced setting.
* defaultsharing - The default sharing option to use. It's highly recommended that you don't set this unless there is a very specific reason not to use the system default.
* canuselocalstore - The default is to required a shared cache location for all nodes in a multi webserver environment. If the cache uses revisions and never updates key data, administrators can use a local storage cache for this cache.
It's important to note that internally the definition is also aware of the component. This is picked up when the definition is read, based upon the location of the caches.php file.
The staticacceleration option.
Data passed to or retrieved from the loader and its chained loaders gets cached by the instance.
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.
Memory use can be controlled by setting the staticaccelerationsize option.
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.
Sharing options.
This controls the options available to the user when configuring the sharing of a definitions cached data.
By default all sharing options are available to select. This particular option allows the developer to limit the options available to the admin configuring the cache.
### 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:
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 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 therefore, intense process.
### Testing
Both the cache API and the cache stores have tests.
Please be aware that several of the cache stores require configuration in order to be able operate in the tests.
Tests for stores requiring configuration that haven't been configured will be skipped.
All configuration is done in your sites config.php through definitions.
As of Moodle 2.8 it is also possible to set the default cache stores used when running tests.
You can do this by adding the following define to your config.php file:
// xxx is one of the installed stored (for example redis) or other cachestore with a test define.
define('TEST_CACHE_USING_APPLICATION_STORE', 'xxx');
This allows you to run tests against a defined test store. It uses the defined value to identify a store to test against with a matching TEST_CACHESTORE define.
Alternatively you can also run tests against an actual cache config.
To do this you must add the following to your config.php file:
define('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH', true');
$CFG->altcacheconfigpath = '/a/temp/directory/yoursite.php'
This tells Moodle to use the config at $CFG->altcacheconfigpath when running tests.
There are a couple of considerations to using this method:
* By setting $CFG->altcacheconfigpath your site will store the cache config in the specified path, not just the test cache config but your site config as well.
* If you have configured your cache before setting $CFG->altcacheconfigpath you will need to copy it from moodledata/muc/config.php to the destination you specified.
* This allows you to share a cache config between sites.
* It also allows you to use tests to test your sites cache config.
Please be aware that if you are using Memcache or Memcached it is recommended to use dedicated Memcached servers.
When caches get purged the memcached servers you have configured get purged, any data stored within them whether it belongs to Moodle or not will be removed.
If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again.
+86
View File
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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');
// The first time the user visits this page we are going to reparse the definitions.
// Just ensures that everything is up to date.
// We flag is session so that this only happens once as people are likely to hit
// this page several times if making changes.
if (empty($SESSION->cacheadminreparsedefinitions)) {
cache_helper::update_definitions();
$SESSION->cacheadminreparsedefinitions = true;
}
$action = optional_param('action', null, PARAM_ALPHA);
admin_externalpage_setup('cacheconfig');
$adminhelper = cache_factory::instance()->get_administration_display_helper();
$notifications = array();
// Empty array to hold any form information returned from actions.
$forminfo = [];
$PAGE->set_primary_active_tab('siteadminnode');
$PAGE->navbar->add(get_string('cacheconfig', 'cache'), new moodle_url('/cache/admin.php'));
// Handle page actions in admin helper class.
if (!empty($action)) {
$forminfo = $adminhelper->perform_cache_actions($action, $forminfo);
}
// Add cache store warnings to the list of notifications.
// Obviously as these are warnings they are show as failures.
foreach (cache_helper::warnings(core_cache\administration_helper::get_store_instance_summaries()) as $warning) {
$notifications[] = array($warning, false);
}
// Decide on display mode based on returned forminfo.
$mform = array_key_exists('form', $forminfo) ? $forminfo['form'] : null;
$title = array_key_exists('title', $forminfo) ? $forminfo['title'] : new lang_string('cacheadmin', 'cache');
$PAGE->set_title($title);
$PAGE->set_heading($SITE->fullname);
/** @var \core_cache\output\renderer $renderer */
$renderer = $PAGE->get_renderer('core_cache');
echo $renderer->header();
echo $renderer->heading($title);
echo $renderer->notifications($notifications);
if ($mform instanceof moodleform) {
$mform->display();
} else {
// Handle main page definition in admin helper class.
echo $adminhelper->generate_admin_page($renderer);
}
echo $renderer->footer();
+431
View File
@@ -0,0 +1,431 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache administration helper.
*
* 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
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_cache;
defined('MOODLE_INTERNAL') || die();
use cache_helper, cache_store, cache_config, cache_factory, cache_definition;
/**
* Administration helper base class.
*
* Defines abstract methods for a subclass to define the admin page.
*
* @package core
* @category cache
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class 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_instance_summaries(): array {
$return = array();
$default = array();
$instance = \cache_config::instance();
$stores = $instance->get_all_stores();
$locks = $instance->get_locks();
foreach ($stores as $name => $details) {
$class = $details['class'];
$store = false;
if ($class::are_requirements_met()) {
$store = new $class($details['name'], $details['configuration']);
}
$lock = (isset($details['lock'])) ? $locks[$details['lock']] : $instance->get_default_lock();
$record = array(
'name' => $name,
'plugin' => $details['plugin'],
'default' => $details['default'],
'isready' => $store ? $store->is_ready() : false,
'requirementsmet' => $class::are_requirements_met(),
'mappings' => 0,
'lock' => $lock,
'modes' => array(
cache_store::MODE_APPLICATION =>
($class::get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION,
cache_store::MODE_SESSION =>
($class::get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION,
cache_store::MODE_REQUEST =>
($class::get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST,
),
'supports' => array(
'multipleidentifiers' => $store ? $store->supports_multiple_identifiers() : false,
'dataguarantee' => $store ? $store->supports_data_guarantee() : false,
'nativettl' => $store ? $store->supports_native_ttl() : false,
'nativelocking' => ($store instanceof \cache_is_lockable),
'keyawareness' => ($store instanceof \cache_is_key_aware),
'searchable' => ($store instanceof \cache_is_searchable)
),
'warnings' => $store ? $store->get_warnings() : array()
);
if (empty($details['default'])) {
$return[$name] = $record;
} else {
$default[$name] = $record;
}
}
ksort($return);
ksort($default);
$return = $return + $default;
$mappings = $instance->get_definition_mappings();
foreach ($mappings as $mapping) {
if (!array_key_exists($mapping['store'], $return)) {
continue;
}
$return[$mapping['store']]['mappings']++;
}
// Now get all definitions, and if not mapped, increment the defaults for the mode.
$modemappings = $instance->get_mode_mappings();
foreach ($instance->get_definitions() as $definition) {
// Construct the definition name to search for.
$defname = $definition['component'] . '/' . $definition['area'];
// Skip if definition is already mapped.
if (array_search($defname, array_column($mappings, 'definition')) !== false) {
continue;
}
$mode = $definition['mode'];
// Get the store name of the default mapping from the mode.
$index = array_search($mode, array_column($modemappings, 'mode'));
$store = $modemappings[$index]['store'];
$return[$store]['mappings']++;
}
return $return;
}
/**
* Returns an array of information about plugins, everything a renderer needs.
*
* @return array for each store, an array containing various information about each store.
* See the code below for details
*/
public static function get_store_plugin_summaries(): array {
$return = array();
$plugins = \core_component::get_plugin_list_with_file('cachestore', 'lib.php', true);
foreach ($plugins as $plugin => $path) {
$class = 'cachestore_'.$plugin;
$return[$plugin] = array(
'name' => get_string('pluginname', 'cachestore_'.$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() && $class::are_requirements_met())
);
}
$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 for each store, an array containing various information about each store.
* See the code below for details
*/
public static function get_definition_summaries(): array {
$factory = cache_factory::instance();
$config = $factory->create_config_instance();
$storenames = array();
foreach ($config->get_all_stores() as $key => $store) {
if (!empty($store['default'])) {
$storenames[$key] = new \lang_string('store_'.$key, 'cache');
} else {
$storenames[$store['name']] = $store['name'];
}
}
/* @var cache_definition[] $definitions */
$definitions = [];
$return = [];
foreach ($config->get_definitions() as $key => $definition) {
$definitions[$key] = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
}
foreach ($definitions as $id => $definition) {
$mappings = array();
foreach (cache_helper::get_stores_suitable_for_definition($definition) as $store) {
$mappings[] = $storenames[$store->my_name()];
}
$return[$id] = array(
'id' => $id,
'name' => $definition->get_name(),
'mode' => $definition->get_mode(),
'component' => $definition->get_component(),
'area' => $definition->get_area(),
'mappings' => $mappings,
'canuselocalstore' => $definition->can_use_localstore(),
'sharingoptions' => self::get_definition_sharing_options($definition->get_sharing_options(), false),
'selectedsharingoption' => self::get_definition_sharing_options($definition->get_selected_sharing_option(), true),
'userinputsharingkey' => $definition->get_user_input_sharing_key()
);
}
return $return;
}
/**
* 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(): array {
global $OUTPUT;
$instance = cache_config::instance();
$adequatestores = cache_helper::get_stores_suitable_for_mode_default();
$icon = new \pix_icon('i/warning', new \lang_string('inadequatestoreformapping', 'cache'));
$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'];
}
if (!array_key_exists($mapping['store'], $adequatestores)) {
$modemappings[$mode][$mapping['store']] = $modemappings[$mode][$mapping['store']].' '.$OUTPUT->render($icon);
}
}
return $modemappings;
}
/**
* Returns an array summarising the locks available in the system.
*
* @return array array of lock summaries.
*/
public static function get_lock_summaries(): array {
$locks = array();
$instance = cache_config::instance();
$stores = $instance->get_all_stores();
foreach ($instance->get_locks() as $lock) {
$default = !empty($lock['default']);
if ($default) {
$name = new \lang_string($lock['name'], 'cache');
} else {
$name = $lock['name'];
}
$uses = 0;
foreach ($stores as $store) {
if (!empty($store['lock']) && $store['lock'] === $lock['name']) {
$uses++;
}
}
$lockdata = array(
'name' => $name,
'default' => $default,
'uses' => $uses,
'type' => get_string('pluginname', $lock['type'])
);
$locks[$lock['name']] = $lockdata;
}
return $locks;
}
/**
* Given a sharing option hash this function returns an array of strings that can be used to describe it.
*
* @param int $sharingoption The sharing option hash to get strings for.
* @param bool $isselectedoptions Set to true if the strings will be used to view the selected options.
* @return array An array of lang_string's.
*/
public static function get_definition_sharing_options(int $sharingoption, bool $isselectedoptions = true): array {
$options = array();
$prefix = ($isselectedoptions) ? 'sharingselected' : 'sharing';
if ($sharingoption & cache_definition::SHARING_ALL) {
$options[cache_definition::SHARING_ALL] = new \lang_string($prefix.'_all', 'cache');
}
if ($sharingoption & cache_definition::SHARING_SITEID) {
$options[cache_definition::SHARING_SITEID] = new \lang_string($prefix.'_siteid', 'cache');
}
if ($sharingoption & cache_definition::SHARING_VERSION) {
$options[cache_definition::SHARING_VERSION] = new \lang_string($prefix.'_version', 'cache');
}
if ($sharingoption & cache_definition::SHARING_INPUT) {
$options[cache_definition::SHARING_INPUT] = new \lang_string($prefix.'_input', 'cache');
}
return $options;
}
/**
* 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(string $component, string $area): array {
$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 ($store['default']) {
unset($possiblestores[$key]);
$possiblestores[$key] = $store;
}
}
return array($currentstores, $possiblestores, $defaults);
}
/**
* This function must be implemented to display options for store plugins.
*
* @param string $name the name of the store plugin.
* @param array $plugindetails array of store plugin details.
* @return array array of actions.
*/
public function get_store_plugin_actions(string $name, array $plugindetails): array {
return array();
}
/**
* This function must be implemented to display options for store instances.
*
* @param string $name the store instance name.
* @param array $storedetails array of store instance details.
* @return array array of actions.
*/
public function get_store_instance_actions(string $name, array $storedetails): array {
return array();
}
/**
* This function must be implemented to display options for definition mappings.
*
* @param context $context the context for the definition.
* @param array $definitionsummary the definition summary.
* @return array array of actions.
*/
public function get_definition_actions(\context $context, array $definitionsummary): array {
return array();
}
/**
* This function must be implemented to get addable locks.
*
* @return array array of locks that are addable.
*/
public function get_addable_lock_options(): array {
return array();
}
/**
* This function must be implemented to perform any page actions by a child class.
*
* @param string $action the action to perform.
* @param array $forminfo empty array to be set by actions.
* @return array array of form info.
*/
abstract public function perform_cache_actions(string $action, array $forminfo): array;
/**
* This function must be implemented to display the cache admin page.
*
* @param \core_cache\output\renderer $renderer the renderer used to generate the page.
* @return string the HTML for the page.
*/
abstract public function generate_admin_page(\core_cache\output\renderer $renderer): string;
/**
* Gets usage information about the whole cache system.
*
* This is a slow function and should only be used on an admin information page.
*
* The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For
* each store, the following fields are available:
*
* - name (store name)
* - class (e.g. cachestore_redis)
* - supported (true if we have any information)
* - items (number of items stored)
* - mean (mean size of item)
* - sd (standard deviation for item sizes)
* - margin (margin of error for mean at 95% confidence)
* - storetotal (total usage for store if known, otherwise null)
*
* The storetotal field will be the same for every cache that uses the same store.
*
* @param int $samplekeys Number of keys to sample when checking size of large caches
* @return array Details of cache usage
*/
abstract public function get_usage(int $samplekeys): array;
}
+80
View File
@@ -0,0 +1,80 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
/**
* Create and keep an instance of this class to allow temporary caches when caches are disabled.
*
* This class works together with code in {@see cache_factory_disabled}.
*
* The intention is that temporary cache should be short-lived (not for the entire install process),
* which avoids two problems: first, that we might run out of memory for the caches, and second,
* that some code e.g. install.php/upgrade.php files, is entitled to assume that caching is not
* used and make direct database changes.
*
* @package core_cache
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class allow_temporary_caches {
/** @var int Number of references of this class; if more than 0, temporary caches are allowed */
protected static $references = 0;
/**
* Constructs an instance of this class.
*
* Temporary caches will be allowed until this instance goes out of scope. Store this token
* in a local variable, so that the caches have a limited life; do not save it outside your
* function.
*
* If cache is not disabled then normal (non-temporary) caches will be used, and this class
* does nothing.
*
* If an object of this class already exists then creating (or destroying) another one will
* have no effect.
*/
public function __construct() {
self::$references++;
}
/**
* Destroys an instance of this class.
*
* You do not need to call this manually; PHP will call it automatically when your variable
* goes out of scope. If you do need to remove your token at other times, use unset($token);
*
* If there are no other instances of this object, then all temporary caches will be discarded.
*/
public function __destruct() {
global $CFG;
require_once($CFG->dirroot . '/cache/disabledlib.php');
self::$references--;
if (self::$references === 0) {
\cache_factory_disabled::clear_temporary_caches();
}
}
/**
* Checks if temp caches are currently allowed.
*
* @return bool True if allowed
*/
public static function is_allowed(): bool {
return self::$references > 0;
}
}
+596
View File
@@ -0,0 +1,596 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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();
/**
* An array of configured cache lock instances.
* @var array
*/
protected $configlocks = array();
/**
* The site identifier used when the cache config was last saved.
* @var string
*/
protected $siteidentifier = null;
/**
* Please use cache_config::instance to get an instance of the cache config that is ready to be used.
*/
public function __construct() {
// Nothing to do here but look pretty.
}
/**
* 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 by using static.
return file_exists(static::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;
if (!empty($CFG->altcacheconfigpath)) {
$path = $CFG->altcacheconfigpath;
if (is_dir($path) && is_writable($path)) {
// Its a writable directory, thats fine.
return $path.'/cacheconfig.php';
} else if (is_writable(dirname($path)) && (!file_exists($path) || is_writable($path))) {
// Its a file, either it doesn't exist and the directory is writable or the file exists and is writable.
return $path;
}
}
// Return the default location within dataroot.
return $CFG->dataroot.'/muc/config.php';
}
/**
* Loads the configuration file and parses its contents into the expected structure.
*
* @param array|false $configuration Can be used to force a configuration. Should only be used when truly required.
* @return boolean
*/
public function load($configuration = false) {
global $CFG;
if ($configuration === false) {
$configuration = $this->include_configuration();
}
$this->configstores = array();
$this->configdefinitions = array();
$this->configlocks = array();
$this->configmodemappings = array();
$this->configdefinitionmappings = array();
$siteidentifier = 'unknown';
if (array_key_exists('siteidentifier', $configuration)) {
$siteidentifier = $configuration['siteidentifier'];
}
$this->siteidentifier = $siteidentifier;
// Filter the lock instances.
$defaultlock = null;
foreach ($configuration['locks'] as $conf) {
if (!is_array($conf)) {
// Something is very wrong here.
continue;
}
if (!array_key_exists('name', $conf)) {
// Not a valid definition configuration.
continue;
}
$name = $conf['name'];
if (array_key_exists($name, $this->configlocks)) {
debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER);
continue;
}
$conf['default'] = (!empty($conf['default']));
if ($defaultlock === null || $conf['default']) {
$defaultlock = $name;
}
$this->configlocks[$name] = $conf;
}
// 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 = 'cachestore_'.$plugin;
$exists = array_key_exists($plugin, $availableplugins);
if (!$exists) {
// 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_parents($class))) {
continue;
}
if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) {
$store['configuration'] = array();
}
$store['class'] = $class;
$store['default'] = !empty($store['default']);
if (!array_key_exists('lock', $store) || !array_key_exists($store['lock'], $this->configlocks)) {
$store['lock'] = $defaultlock;
}
$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;
}
if ($conf['mode'] === cache_store::MODE_SESSION || $conf['mode'] === cache_store::MODE_REQUEST) {
// We force this for session and request caches.
// They are only allowed to use the default as we don't want people changing them.
$conf['sharingoptions'] = cache_definition::SHARING_DEFAULT;
$conf['selectedsharingoption'] = cache_definition::SHARING_DEFAULT;
$conf['userinputsharingkey'] = '';
} else {
// Default the sharing option as it was added for 2.5.
// This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
if (!isset($conf['sharingoptions'])) {
$conf['sharingoptions'] = cache_definition::SHARING_DEFAULTOPTIONS;
}
// Default the selected sharing option as it was added for 2.5.
// This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
if (!isset($conf['selectedsharingoption'])) {
$conf['selectedsharingoption'] = cache_definition::SHARING_DEFAULT;
}
// Default the user input sharing key as it was added for 2.5.
// This can be removed sometime after 2.5 is the minimum version someone can upgrade from.
if (!isset($conf['userinputsharingkey'])) {
$conf['userinputsharingkey'] = '';
}
}
$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;
}
/**
* Returns the site identifier used by the cache API.
* @return string
*/
public function get_site_identifier() {
return $this->siteidentifier;
}
/**
* 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 = null;
// We need to allow for late static bindings to allow for class path mudling happending for unit tests.
$cachefile = static::get_config_file_path();
if (!file_exists($cachefile)) {
throw new cache_exception('Default cache config could not be found. It should have already been created by now.');
}
if (!include($cachefile)) {
throw new cache_exception('Unable to load the cache configuration file');
}
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();
}
if (!array_key_exists('locks', $configuration) || !is_array($configuration['locks'])) {
$configuration['locks'] = 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 the definitions mapped into the given store name.
*
* @param string $storename
* @return array Associative array of definitions, id=>definition
*/
public function get_definitions_by_store($storename) {
$definitions = array();
// This function was accidentally made static at some stage in the past.
// It was converted to an instance method but to be backwards compatible
// we must step around this in code.
if (!isset($this)) {
$config = cache_config::instance();
} else {
$config = $this;
}
$stores = $config->get_all_stores();
if (!array_key_exists($storename, $stores)) {
// The store does not exist.
return false;
}
$defmappings = $config->get_definition_mappings();
// Create an associative array for the definition mappings.
$thedefmappings = array();
foreach ($defmappings as $defmapping) {
$thedefmappings[$defmapping['definition']] = $defmapping;
}
// Search for matches in default mappings.
$defs = $config->get_definitions();
foreach($config->get_mode_mappings() as $modemapping) {
if ($modemapping['store'] !== $storename) {
continue;
}
foreach($defs as $id => $definition) {
if ($definition['mode'] !== $modemapping['mode']) {
continue;
}
// Exclude custom definitions mapping: they will be managed few lines below.
if (array_key_exists($id, $thedefmappings)) {
continue;
}
$definitions[$id] = $definition;
}
}
// Search for matches in the custom definitions mapping
foreach ($defmappings as $defmapping) {
if ($defmapping['store'] !== $storename) {
continue;
}
$definition = $config->get_definition_by_id($defmapping['definition']);
if ($definition) {
$definitions[$defmapping['definition']] = $definition;
}
}
return $definitions;
}
/**
* 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.
$factory = cache_factory::instance();
if ($factory->stores_disabled()) {
// 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 cachestore_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;
}
/**
* Returns an array of the configured locks.
* @return array Array of name => config
*/
public function get_locks() {
return $this->configlocks;
}
/**
* Returns the lock store configuration to use with a given store.
* @param string $storename
* @return array
* @throws cache_exception
*/
public function get_lock_for_store($storename) {
if (array_key_exists($storename, $this->configstores)) {
if (array_key_exists($this->configstores[$storename]['lock'], $this->configlocks)) {
$lock = $this->configstores[$storename]['lock'];
return $this->configlocks[$lock];
}
}
return $this->get_default_lock();
}
/**
* Gets the default lock instance.
*
* @return array
* @throws cache_exception
*/
public function get_default_lock() {
foreach ($this->configlocks as $lockconf) {
if (!empty($lockconf['default'])) {
return $lockconf;
}
}
throw new cache_exception('ex_nodefaultlock');
}
}
+1009
View File
File diff suppressed because it is too large Load Diff
+291
View File
@@ -0,0 +1,291 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 cachestore_dummy extends cache_store {
/**
* The name of this store.
* @var string
*/
protected $name;
/**
* Gets set to true if this store is going to store data.
* This happens when the definition doesn't require static acceleration as the loader will not be storing information and
* something has to.
* @var bool
*/
protected $persist = false;
/**
* The stored data array
* @var array
*/
protected $store = array();
/**
* Cache definition
* @var cache_definition
*/
protected $definition;
/**
* 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 using static acceleration then we need to be store data here.
// The reasoning behind this is that:
// - If the definition is using static acceleration then the cache loader is going to
// store things in its static array.
// - If the definition is not using static acceleration then the cache loader won't try to store anything
// and we will need to store it here in order to make sure it is accessible.
if ($definition->get_mode() !== self::MODE_APPLICATION) {
// Neither the request cache nor the session cache provide static acceleration.
$this->persist = true;
} else {
$this->persist = !$definition->use_static_acceleration();
}
$this->definition = $definition;
}
/**
* Returns true if this has been initialised.
* @return bool
*/
public function is_initialised() {
return (!empty($this->definition));
}
/**
* Returns true the given mode is supported.
* @param int $mode
* @return bool
*/
public static function is_supported_mode($mode) {
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);
}
/**
* 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.
*
* @deprecated since 3.2
* @see cachestore_dummy::instance_deleted()
*/
public function cleanup() {
debugging('cachestore_dummy::cleanup() is deprecated. Please use cachestore_dummy::instance_deleted() instead.',
DEBUG_DEVELOPER);
$this->instance_deleted();
}
/**
* Performs any necessary operation when the store instance is being deleted.
*
* This method may be called before the store has been initialised.
*
* @since Moodle 3.2
*/
public function instance_deleted() {
$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 cachestore_dummy('Dummy store test');
if ($cache->is_ready()) {
$cache->initialise($definition);
}
return $cache;
}
/**
* Generates the appropriate configuration required for unit testing.
*
* @return array Array of unit test configuration data to be used by initialise().
*/
public static function unit_test_configuration() {
return [];
}
/**
* Returns the name of this instance.
* @return string
*/
public function my_name() {
return $this->name;
}
}
+693
View File
@@ -0,0 +1,693 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 {
/** The cache has not been initialised yet. */
const STATE_UNINITIALISED = 0;
/** The cache is in the process of initialising itself. */
const STATE_INITIALISING = 1;
/** The cache is in the process of saving its configuration file. */
const STATE_SAVING = 2;
/** The cache is ready to use. */
const STATE_READY = 3;
/** The cache is currently updating itself */
const STATE_UPDATING = 4;
/** The cache encountered an error while initialising. */
const STATE_ERROR_INITIALISING = 9;
/** The cache has been disabled. */
const STATE_DISABLED = 10;
/** The cache stores have been disabled */
const STATE_STORES_DISABLED = 11;
/**
* 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 stores organised by definitions.
* @var array
*/
protected $definitionstores = 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();
/**
* An array of lock plugins.
* @var array
*/
protected $lockplugins = array();
/**
* The current state of the cache API.
* @var int
*/
protected $state = 0;
/**
* The current cache display helper.
* @var core_cache\local\administration_display_helper
*/
protected static $displayhelper = null;
/**
* Returns an instance of the cache_factory class.
*
* @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) {
global $CFG;
if ($forcereload || self::$instance === null) {
// Initialise a new factory to facilitate our needs.
if (defined('CACHE_DISABLE_ALL') && CACHE_DISABLE_ALL !== false) {
// The cache has been disabled. Load disabledlib and start using the factory designed to handle this
// situation. It will use disabled alternatives where available.
require_once($CFG->dirroot.'/cache/disabledlib.php');
self::$instance = new cache_factory_disabled();
} else if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING')) {
// We're using the test factory.
require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php');
self::$instance = new cache_phpunit_factory();
if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) {
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
} else if (!empty($CFG->alternative_cache_factory_class)) {
$factoryclass = $CFG->alternative_cache_factory_class;
self::$instance = new $factoryclass();
} else {
// We're using the regular factory.
self::$instance = new cache_factory();
if (defined('CACHE_DISABLE_STORES') && CACHE_DISABLE_STORES !== false) {
// The cache stores have been disabled.
self::$instance->set_state(self::STATE_STORES_DISABLED);
}
}
}
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->reset_cache_instances();
$factory->configs = array();
$factory->definitions = array();
$factory->definitionstores = array();
$factory->lockplugins = array(); // MUST be null in order to force its regeneration.
// Reset the state to uninitialised.
$factory->state = self::STATE_UNINITIALISED;
}
/**
* Resets the stores, clearing the array of created stores.
*
* Cache objects still held onto by the code that initialised them will remain as is
* however all future requests for a cache/store will lead to a new instance being re-initialised.
*/
public function reset_cache_instances() {
$this->cachesfromdefinitions = array();
$this->cachesfromparams = array();
$this->stores = 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 $unused Used to be data source aggregate however that was removed and this is now unused.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_definition($component, $area, array $identifiers = array(), $unused = null) {
$identifierstring = empty($identifiers) ? '' : '/'.http_build_query($identifiers);
$definitionname = $component.'/'.$area.$identifierstring;
if (isset($this->cachesfromdefinitions[$definitionname])) {
$cache = $this->cachesfromdefinitions[$definitionname];
return $cache;
}
$definition = $this->create_definition($component, $area);
// Identifiers are cached as part of the cache creation, so we store a cloned version of the cache.
$cacheddefinition = clone($definition);
$cacheddefinition->set_identifiers($identifiers);
$cache = $this->create_cache($cacheddefinition);
// Loaders are always held onto to speed up subsequent requests.
$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 array $options An array of options, available options are:
* - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_
* - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars
* - staticacceleration : If set to true the cache will hold onto data passing through it.
* - staticaccelerationsize : The maximum number of items to hold onto for acceleration purposes.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_params($mode, $component, $area, array $identifiers = array(), array $options = array()) {
$identifierstring = empty($identifiers) ? '' : '_'.http_build_query($identifiers);
$key = "{$mode}_{$component}_{$area}{$identifierstring}";
if (isset($this->cachesfromparams[$key])) {
return $this->cachesfromparams[$key];
}
// Regular cache definitions are cached inside create_definition(). This is not the case for Adhoc definitions
// using load_adhoc(). They are built as a new object on each call.
// We do not need to clone the definition because we know it's new.
$definition = cache_definition::load_adhoc($mode, $component, $area, $options);
$definition->set_identifiers($identifiers);
$cache = $this->create_cache($definition);
$this->cachesfromparams[$key] = $cache;
return $cache;
}
/**
* Common public 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_stores_suitable_for_definition($definition);
foreach ($stores as $key => $store) {
if (!$store::are_requirements_met()) {
unset($stores[$key]);
}
}
if (count($stores) === 0) {
// Hmm still no stores, better provide a dummy store to mimic 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 object 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|cache_store
*/
public function create_store_from_config($name, array $details, cache_definition $definition) {
if (!array_key_exists($name, $this->stores)) {
// Properties: name, plugin, configuration, class.
$class = $details['class'];
if (!$class::are_requirements_met()) {
return false;
}
$store = new $class($details['name'], $details['configuration']);
$this->stores[$name] = $store;
}
/* @var cache_store $store */
$store = $this->stores[$name];
// We check are_requirements_met although we expect is_ready is going to check as well.
if (!$store::are_requirements_met() || !$store->is_ready() || !$store->is_supported_mode($definition->get_mode())) {
return false;
}
// We always create a clone of the original store.
// If we were to clone a store that had already been initialised with a definition then
// we'd run into a myriad of issues.
// We use a method of the store to create a clone rather than just creating it ourselves
// so that if any store out there doesn't handle cloning they can override this method in
// order to address the issues.
$store = $this->stores[$name]->create_clone($details);
$store->initialise($definition);
$definitionid = $definition->get_id();
if (!isset($this->definitionstores[$definitionid])) {
$this->definitionstores[$definitionid] = array();
}
$this->definitionstores[$definitionid][] = $store;
return $store;
}
/**
* Returns an array of cache stores that have been initialised for use in definitions.
* @param cache_definition $definition
* @return array
*/
public function get_store_instances_in_use(cache_definition $definition) {
$id = $definition->get_id();
if (!isset($this->definitionstores[$id])) {
return array();
}
return $this->definitionstores[$id];
}
/**
* Returns the cache instances that have been used within this request.
* @since Moodle 2.6
* @return array
*/
public function get_caches_in_use() {
return $this->cachesfromdefinitions;
}
/**
* Gets all adhoc caches that have been used within this request.
*
* @return cache_store[] Caches currently in use
*/
public function get_adhoc_caches_in_use() {
return $this->cachesfromparams;
}
/**
* 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;
// The class to use.
$class = 'cache_config';
// Are we running tests of some form?
$testing = (defined('PHPUNIT_TEST') && PHPUNIT_TEST) || defined('BEHAT_SITE_RUNNING');
// Check if this is a PHPUnit test and redirect to the phpunit config classes if it is.
if ($testing) {
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_testing';
}
// Check if we need to create a config file with defaults.
$needtocreate = !$class::config_file_exists();
if ($writer || $needtocreate) {
require_once($CFG->dirroot.'/cache/locallib.php');
if (!$testing) {
$class .= '_writer';
}
}
$error = false;
if ($needtocreate) {
// Create the default configuration.
// Update the state, we are now initialising the cache.
self::set_state(self::STATE_INITIALISING);
/** @var cache_config_writer $class */
$configuration = $class::create_default_configuration();
if ($configuration !== true) {
// Failed to create the default configuration. Disable the cache stores and update the state.
self::set_state(self::STATE_ERROR_INITIALISING);
$this->configs[$class] = new $class;
$this->configs[$class]->load($configuration);
$error = true;
}
}
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();
}
if (!$error) {
// The cache is now ready to use. Update the state.
self::set_state(self::STATE_READY);
}
// 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 $unused This used to be data source aggregate - however that functionality has been removed and
* this argument is now unused.
* @return cache_definition
* @throws coding_exception If the definition cannot be found.
*/
public function create_definition($component, $area, $unused = null) {
$id = $component.'/'.$area;
if (!isset($this->definitions[$id])) {
// This is the first time this definition has been requested.
if ($this->is_initialising()) {
// We're initialising the cache right now. Don't try to create another config instance.
// We'll just use an ad-hoc cache for the time being.
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
} else {
// Load all the known definitions and find the desired one.
$instance = $this->create_config_instance();
$definition = $instance->get_definition_by_id($id);
if (!$definition) {
// Oh-oh the definition doesn't exist.
// There are several things that could be going on here.
// We may be installing/upgrading a site and have hit a definition that hasn't been used before.
// Of the developer may be trying to use a newly created definition.
if ($this->is_updating()) {
// The cache is presently initialising and the requested cache definition has not been found.
// This means that the cache initialisation has requested something from a cache (I had recursive nightmares about this).
// To serve this purpose and avoid errors we are going to make use of an ad-hoc cache rather than
// search for the definition which would possibly cause an infitite loop trying to initialise the cache.
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
} else {
// Either a typo of the developer has just created the definition and is using it for the first time.
$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);
}
if (!$this->is_disabled()) {
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);
}
$definition = cache_definition::load($id, $definition);
}
} else {
$definition = cache_definition::load($id, $definition);
}
}
$this->definitions[$id] = $definition;
}
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 cachestore_dummy
*/
protected function create_dummy_store(cache_definition $definition) {
global $CFG;
require_once($CFG->dirroot.'/cache/classes/dummystore.php');
$store = new cachestore_dummy();
$store->initialise($definition);
return $store;
}
/**
* Returns a lock instance ready for use.
*
* @param array $config
* @return cache_lock_interface
*/
public function create_lock_instance(array $config) {
global $CFG;
if (!array_key_exists('name', $config) || !array_key_exists('type', $config)) {
throw new coding_exception('Invalid cache lock instance provided');
}
$name = $config['name'];
$type = $config['type'];
unset($config['name']);
unset($config['type']);
if (!isset($this->lockplugins[$type])) {
$pluginname = substr($type, 10);
$file = $CFG->dirroot."/cache/locks/{$pluginname}/lib.php";
if (file_exists($file) && is_readable($file)) {
require_once($file);
}
if (!class_exists($type)) {
throw new coding_exception('Invalid lock plugin requested.');
}
$this->lockplugins[$type] = $type;
}
if (!array_key_exists($type, $this->lockplugins)) {
throw new coding_exception('Invalid cache lock type.');
}
$class = $this->lockplugins[$type];
return new $class($name, $config);
}
/**
* Returns the current state of the cache API.
*
* @return int
*/
public function get_state() {
return $this->state;
}
/**
* Updates the state fo the cache API.
*
* @param int $state
* @return bool
*/
public function set_state($state) {
if ($state <= $this->state) {
return false;
}
$this->state = $state;
return true;
}
/**
* Informs the factory that the cache is currently updating itself.
*
* This forces the state to upgrading and can only be called once the cache is ready to use.
* Calling it ensure we don't try to reinstantite things when requesting cache definitions that don't exist yet.
*/
public function updating_started() {
if ($this->state !== self::STATE_READY) {
return false;
}
$this->state = self::STATE_UPDATING;
return true;
}
/**
* Informs the factory that the upgrading has finished.
*
* This forces the state back to ready.
*/
public function updating_finished() {
$this->state = self::STATE_READY;
}
/**
* Returns true if the cache API has been disabled.
*
* @return bool
*/
public function is_disabled() {
return $this->state === self::STATE_DISABLED;
}
/**
* Returns true if the cache is currently initialising itself.
*
* This includes both initialisation and saving the cache config file as part of that initialisation.
*
* @return bool
*/
public function is_initialising() {
return $this->state === self::STATE_INITIALISING || $this->state === self::STATE_SAVING;
}
/**
* Returns true if the cache is currently updating itself.
*
* @return bool
*/
public function is_updating() {
return $this->state === self::STATE_UPDATING;
}
/**
* Disables as much of the cache API as possible.
*
* All of the magic associated with the disabled cache is wrapped into this function.
* In switching out the factory for the disabled factory it gains full control over the initialisation of objects
* and can use all of the disabled alternatives.
* Simple!
*
* This function has been marked as protected so that it cannot be abused through the public API presently.
* Perhaps in the future we will allow this, however as per the build up to the first release containing
* MUC it was decided that this was just to risky and abusable.
*/
protected static function disable() {
global $CFG;
require_once($CFG->dirroot.'/cache/disabledlib.php');
self::$instance = new cache_factory_disabled();
}
/**
* Returns true if the cache stores have been disabled.
*
* @return bool
*/
public function stores_disabled() {
return $this->state === self::STATE_STORES_DISABLED || $this->is_disabled();
}
/**
* Disables cache stores.
*
* The cache API will continue to function however none of the actual stores will be used.
* Instead the dummy store will be provided for all cache requests.
* This is useful in situations where you cannot be sure any stores are working.
*
* In order to re-enable the cache you must call the cache factories static reset method:
* <code>
* // Disable the cache factory.
* cache_factory::disable_stores();
* // Re-enable the cache factory by resetting it.
* cache_factory::reset();
* </code>
*/
public static function disable_stores() {
// First reset to clear any static acceleration array.
$factory = self::instance();
$factory->reset_cache_instances();
$factory->set_state(self::STATE_STORES_DISABLED);
}
/**
* Returns an instance of the current display_helper.
*
* @return core_cache\administration_helper
*/
public static function get_administration_display_helper(): core_cache\administration_helper {
if (is_null(self::$displayhelper)) {
self::$displayhelper = new \core_cache\local\administration_display_helper();
}
return self::$displayhelper;
}
/**
* Gets the cache_config_writer to use when caching is disabled.
* This should only be called from cache_factory_disabled.
*
* @return cache_config_writer
*/
public static function get_disabled_writer(): cache_config_writer {
global $CFG;
// Figure out if we are in a recursive loop using late static binding.
// This happens when get_disabled_writer is not overridden. We just want the default.
$loop = false;
if (!empty($CFG->alternative_cache_factory_class)) {
$loop = get_called_class() === $CFG->alternative_cache_factory_class;
}
if (!$loop && !empty($CFG->alternative_cache_factory_class)) {
// Get the class to use from the alternative factory.
$factoryinstance = new $CFG->alternative_cache_factory_class();
return $factoryinstance::get_disabled_writer();
} else {
// We got here from cache_factory_disabled.
// We should use the default writer here.
// Make sure we have a default config if needed.
if (!cache_config::config_file_exists()) {
cache_config_writer::create_default_configuration(true);
}
return new cache_config_writer();
}
}
}
+894
View File
@@ -0,0 +1,894 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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;
/**
* The site identifier used by the cache.
* Set the first time get_site_identifier is called.
* @var string
*/
protected static $siteidentifier = null;
/**
* 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_cachestore_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 cache_store[]
*/
protected static function initialise_cachestore_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 a cache_lock instance suitable for use with the store.
*
* @param cache_store $store
* @return cache_lock_interface
*/
public static function get_cachelock_for_store(cache_store $store) {
$instance = cache_config::instance();
$lockconf = $instance->get_lock_for_store($store->my_name());
$factory = cache_factory::instance();
return $factory->create_lock_instance($lockconf);
}
/**
* 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 array
*/
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;
}
if (!is_valid_plugin_name($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|string|int $keys
* @return boolean
* @throws coding_exception
*/
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.
*
* Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation
* are only supported on caches without identifiers.
*
* @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();
$inuse = $factory->get_caches_in_use();
$purgetoken = null;
foreach ($instance->get_definitions() as $name => $definitionarr) {
$definition = cache_definition::load($name, $definitionarr);
if ($definition->invalidates_on_event($event)) {
// First up check if there is a cache loader for this definition already.
// If there is we need to invalidate the keys from there.
$definitionkey = $definition->get_component().'/'.$definition->get_area();
if (isset($inuse[$definitionkey])) {
$inuse[$definitionkey]->delete_many($keys);
}
// We should only log events for application and session caches.
// Request caches shouldn't have events as all data is lost at the end of the request.
// Events should only be logged once of course and likely several definitions are watching so we
// track its logging with $invalidationeventset.
$logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
if ($logevent) {
// 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.
if (null === $purgetoken) {
$purgetoken = cache::get_purge_token(true);
}
foreach ($keys as $key) {
$data[$key] = $purgetoken;
}
// 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);
// Initialise, in case of a store.
if ($cache instanceof cache_store) {
$factory = cache_factory::instance();
$definition = $factory->create_definition($component, $area, null);
$cacheddefinition = clone $definition;
$cacheddefinition->set_identifiers($identifiers);
$cache->initialise($cacheddefinition);
}
// Purge baby, purge.
$cache->purge();
return true;
}
/**
* Purges a cache of all information on a given event.
*
* Events cannot determine what identifiers might need to be cleared. Event based purge and invalidation
* are only supported on caches without identifiers.
*
* @param string $event
*/
public static function purge_by_event($event) {
$instance = cache_config::instance();
$invalidationeventset = false;
$factory = cache_factory::instance();
$inuse = $factory->get_caches_in_use();
$purgetoken = null;
foreach ($instance->get_definitions() as $name => $definitionarr) {
$definition = cache_definition::load($name, $definitionarr);
if ($definition->invalidates_on_event($event)) {
// First up check if there is a cache loader for this definition already.
// If there is we need to invalidate the keys from there.
$definitionkey = $definition->get_component().'/'.$definition->get_area();
if (isset($inuse[$definitionkey])) {
$inuse[$definitionkey]->purge();
} else {
cache::make($definition->get_component(), $definition->get_area())->purge();
}
// We should only log events for application and session caches.
// Request caches shouldn't have events as all data is lost at the end of the request.
// Events should only be logged once of course and likely several definitions are watching so we
// track its logging with $invalidationeventset.
$logevent = ($invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST);
// We need to flag the event in the "Event invalidation" cache if it hasn't already happened.
if ($logevent && $invalidationeventset === false) {
// Get the event invalidation cache.
$cache = cache::make('core', 'eventinvalidation');
// Create a key to invalidate all.
if (null === $purgetoken) {
$purgetoken = cache::get_purge_token(true);
}
$data = array(
'purged' => $purgetoken,
);
// 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 $storeclass
* @param string $definition A string that identifies the definition.
* @param int $mode One of cache_store::MODE_*. Since 2.9.
*/
protected static function ensure_ready_for_stats($store, $storeclass, $definition, $mode = cache_store::MODE_APPLICATION) {
// This function is performance-sensitive, so exit as quickly as possible
// if we do not need to do anything.
if (isset(self::$stats[$definition]['stores'][$store])) {
return;
}
if (!array_key_exists($definition, self::$stats)) {
self::$stats[$definition] = array(
'mode' => $mode,
'stores' => array(
$store => array(
'class' => $storeclass,
'hits' => 0,
'misses' => 0,
'sets' => 0,
'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
'locks' => 0,
)
)
);
} else if (!array_key_exists($store, self::$stats[$definition]['stores'])) {
self::$stats[$definition]['stores'][$store] = array(
'class' => $storeclass,
'hits' => 0,
'misses' => 0,
'sets' => 0,
'iobytes' => cache_store::IO_BYTES_NOT_SUPPORTED,
'locks' => 0,
);
}
}
/**
* Returns a string to describe the definition.
*
* This method supports the definition as a string due to legacy requirements.
* It is backwards compatible when a string is passed but is not accurate.
*
* @since 2.9
* @param cache_definition|string $definition
* @return string
*/
protected static function get_definition_stat_id_and_mode($definition) {
if (!($definition instanceof cache_definition)) {
// All core calls to this method have been updated, this is the legacy state.
// We'll use application as the default as that is the most common, really this is not accurate of course but
// at this point we can only guess and as it only affects calls to cache stat outside of core (of which there should
// be none) I think that is fine.
debugging('Please update you cache stat calls to pass the definition rather than just its ID.', DEBUG_DEVELOPER);
return array((string)$definition, cache_store::MODE_APPLICATION);
}
return array($definition->get_id(), $definition->get_mode());
}
/**
* Record a cache hit in the stats for the given store and definition.
*
* In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
* cache_definition instance. It is preferable to pass a cache definition instance.
*
* In Moodle 3.9 the first argument changed to also accept a cache_store.
*
* @internal
* @param string|cache_store $store
* @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
* actual cache_definition object now.
* @param int $hits The number of hits to record (by default 1)
* @param int $readbytes Number of bytes read from the cache or cache_store::IO_BYTES_NOT_SUPPORTED
*/
public static function record_cache_hit($store, $definition, int $hits = 1, int $readbytes = cache_store::IO_BYTES_NOT_SUPPORTED): void {
$storeclass = '';
if ($store instanceof cache_store) {
$storeclass = get_class($store);
$store = $store->my_name();
}
list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
self::$stats[$definitionstr]['stores'][$store]['hits'] += $hits;
if ($readbytes !== cache_store::IO_BYTES_NOT_SUPPORTED) {
if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $readbytes;
} else {
self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $readbytes;
}
}
}
/**
* Record a cache miss in the stats for the given store and definition.
*
* In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
* cache_definition instance. It is preferable to pass a cache definition instance.
*
* In Moodle 3.9 the first argument changed to also accept a cache_store.
*
* @internal
* @param string|cache_store $store
* @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
* actual cache_definition object now.
* @param int $misses The number of misses to record (by default 1)
*/
public static function record_cache_miss($store, $definition, $misses = 1) {
$storeclass = '';
if ($store instanceof cache_store) {
$storeclass = get_class($store);
$store = $store->my_name();
}
list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
self::$stats[$definitionstr]['stores'][$store]['misses'] += $misses;
}
/**
* Record a cache set in the stats for the given store and definition.
*
* In Moodle 2.9 the $definition argument changed from accepting only a string to accepting a string or a
* cache_definition instance. It is preferable to pass a cache definition instance.
*
* In Moodle 3.9 the first argument changed to also accept a cache_store.
*
* @internal
* @param string|cache_store $store
* @param cache_definition $definition You used to be able to pass a string here, however that is deprecated please pass the
* actual cache_definition object now.
* @param int $sets The number of sets to record (by default 1)
* @param int $writebytes Number of bytes written to the cache or cache_store::IO_BYTES_NOT_SUPPORTED
*/
public static function record_cache_set($store, $definition, int $sets = 1,
int $writebytes = cache_store::IO_BYTES_NOT_SUPPORTED) {
$storeclass = '';
if ($store instanceof cache_store) {
$storeclass = get_class($store);
$store = $store->my_name();
}
list($definitionstr, $mode) = self::get_definition_stat_id_and_mode($definition);
self::ensure_ready_for_stats($store, $storeclass, $definitionstr, $mode);
self::$stats[$definitionstr]['stores'][$store]['sets'] += $sets;
if ($writebytes !== cache_store::IO_BYTES_NOT_SUPPORTED) {
if (self::$stats[$definitionstr]['stores'][$store]['iobytes'] === cache_store::IO_BYTES_NOT_SUPPORTED) {
self::$stats[$definitionstr]['stores'][$store]['iobytes'] = $writebytes;
} else {
self::$stats[$definitionstr]['stores'][$store]['iobytes'] += $writebytes;
}
}
}
/**
* 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.
*
* Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
* anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
* painful.
*
* @param bool $usewriter If set to true the cache_config_writer class is used. This class is special as it avoids
* it is still usable when caches have been disabled.
* Please use this option only if you really must. It's purpose is to allow the cache to be purged when it would be
* otherwise impossible.
*/
public static function purge_all($usewriter = false) {
$factory = cache_factory::instance();
$config = $factory->create_config_instance($usewriter);
foreach ($config->get_all_stores() as $store) {
self::purge_store($store['name'], $config);
}
foreach ($factory->get_adhoc_caches_in_use() as $cache) {
$cache->purge();
}
}
/**
* Purges a store given its name.
*
* @param string $storename
* @param cache_config $config
* @return bool
*/
public static function purge_store($storename, cache_config $config = null) {
if ($config === null) {
$config = cache_config::instance();
}
$stores = $config->get_all_stores();
if (!array_key_exists($storename, $stores)) {
// The store does not exist.
return false;
}
$store = $stores[$storename];
$class = $store['class'];
// We check are_requirements_met although we expect is_ready is going to check as well.
if (!$class::are_requirements_met()) {
return false;
}
// Found the store: is it ready?
/* @var cache_store $instance */
$instance = new $class($store['name'], $store['configuration']);
if (!$instance->is_ready()) {
unset($instance);
return false;
}
foreach ($config->get_definitions_by_store($storename) as $id => $definition) {
$definition = cache_definition::load($id, $definition);
$definitioninstance = clone($instance);
$definitioninstance->initialise($definition);
$definitioninstance->purge();
unset($definitioninstance);
}
return true;
}
/**
* Purges all of the stores used by a definition.
*
* Unlike cache_helper::purge_by_definition this purges all of the data from the stores not
* just the data relating to the definition.
* This function is useful when you must purge a definition that requires setup but you don't
* want to set it up.
*
* @param string $component
* @param string $area
*/
public static function purge_stores_used_by_definition($component, $area) {
$factory = cache_factory::instance();
$config = $factory->create_config_instance();
$definition = $factory->create_definition($component, $area);
$stores = $config->get_stores_for_definition($definition);
foreach ($stores as $store) {
self::purge_store($store['name']);
}
}
/**
* 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 still unique.
* @param string|int $key
* @param cache_definition $definition
* @return string
*/
public static function hash_key($key, cache_definition $definition) {
if ($definition->uses_simple_keys()) {
if (debugging() && preg_match('#[^a-zA-Z0-9_]#', $key ?? '')) {
throw new coding_exception('Cache definition '.$definition->get_id().' requires simple keys. Invalid key provided.', $key);
}
// We put the key first so that we can be sure the start of the key changes.
return (string)$key . '-' . $definition->generate_single_key_prefix();
}
$key = $definition->generate_single_key_prefix() . '-' . $key;
return sha1($key);
}
/**
* Finds all definitions and updates them within the cache config file.
*
* @param bool $coreonly If set to true only core definitions will be updated.
*/
public static function update_definitions($coreonly = false) {
global $CFG;
// Include locallib.
require_once($CFG->dirroot.'/cache/locallib.php');
// First update definitions
cache_config_writer::update_definitions($coreonly);
// Second reset anything we have already initialised to ensure we're all up to date.
cache_factory::reset();
}
/**
* Update the site identifier stored by the cache API.
*
* @param string $siteidentifier
* @return string The new site identifier.
*/
public static function update_site_identifier($siteidentifier) {
global $CFG;
// Include locallib.
require_once($CFG->dirroot.'/cache/locallib.php');
$factory = cache_factory::instance();
$factory->updating_started();
$config = $factory->create_config_instance(true);
$siteidentifier = $config->update_site_identifier($siteidentifier);
$factory->updating_finished();
cache_factory::reset();
return $siteidentifier;
}
/**
* Returns the site identifier.
*
* @return string
*/
public static function get_site_identifier() {
global $CFG;
if (!is_null(self::$siteidentifier)) {
return self::$siteidentifier;
}
// If site identifier hasn't been collected yet attempt to get it from the cache config.
$factory = cache_factory::instance();
// If the factory is initialising then we don't want to try to get it from the config or we risk
// causing the cache to enter an infinite initialisation loop.
if (!$factory->is_initialising()) {
$config = $factory->create_config_instance();
self::$siteidentifier = $config->get_site_identifier();
}
if (is_null(self::$siteidentifier)) {
// If the site identifier is still null then config isn't aware of it yet.
// We'll see if the CFG is loaded, and if not we will just use unknown.
// It's very important here that we don't use get_config. We don't want an endless cache loop!
if (!empty($CFG->siteidentifier)) {
self::$siteidentifier = self::update_site_identifier($CFG->siteidentifier);
} else {
// It's not being recorded in MUC's config and the config data hasn't been loaded yet.
// Likely we are initialising.
return 'unknown';
}
}
return self::$siteidentifier;
}
/**
* Returns the site version.
*
* @return string
*/
public static function get_site_version() {
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 last access keys.
$keys = $store->find_by_prefix(cache_session::LASTACCESS);
$todelete = [];
foreach ($store->get_many($keys) as $key => $value) {
$expiresvalue = 0;
if ($value instanceof cache_ttl_wrapper) {
$expiresvalue = $value->data;
} else if ($value instanceof cache_cached_object) {
$expiresvalue = $value->restore_object();
} else {
$expiresvalue = $value;
}
$expires = (int) $expiresvalue;
if ($expires > 0 && $expires < $purgetime) {
$prefix = substr($key, strlen(cache_session::LASTACCESS));
$foundbyprefix = $store->find_by_prefix($prefix);
$todelete = array_merge($todelete, [$key], $foundbyprefix);
}
}
if ($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.");
}
}
}
}
}
/**
* Returns an array of stores that would meet the requirements for every definition.
*
* These stores would be 100% suitable to map as defaults for cache modes.
*
* @return array[] An array of stores, keys are the store names.
*/
public static function get_stores_suitable_for_mode_default() {
$factory = cache_factory::instance();
$config = $factory->create_config_instance();
$requirements = 0;
foreach ($config->get_definitions() as $definition) {
$definition = cache_definition::load($definition['component'].'/'.$definition['area'], $definition);
$requirements = $requirements | $definition->get_requirements_bin();
}
$stores = array();
foreach ($config->get_all_stores() as $name => $store) {
if (!empty($store['features']) && ($store['features'] & $requirements)) {
$stores[$name] = $store;
}
}
return $stores;
}
/**
* Returns stores suitable for use with a given definition.
*
* @param cache_definition $definition
* @return cache_store[]
*/
public static function get_stores_suitable_for_definition(cache_definition $definition) {
$factory = cache_factory::instance();
$stores = array();
if ($factory->is_initialising() || $factory->stores_disabled()) {
// No suitable stores here.
return $stores;
} else {
$stores = self::get_cache_stores($definition);
// If mappingsonly is set, having 0 stores is ok.
if ((count($stores) === 0) && (!$definition->is_for_mappings_only())) {
// No suitable stores we found for the definition. We need to come up with a sensible default.
// If this has happened we can be sure that the user has mapped custom stores to either the
// mode of the definition. The first alternative to try is the system default for the mode.
// e.g. the default file store instance for application definitions.
$config = $factory->create_config_instance();
foreach ($config->get_stores($definition->get_mode()) as $name => $details) {
if (!empty($details['default'])) {
$stores[] = $factory->create_store_from_config($name, $details, $definition);
break;
}
}
}
}
return $stores;
}
/**
* Returns an array of warnings from the cache API.
*
* The warning returned here are for things like conflicting store instance configurations etc.
* These get shown on the admin notifications page for example.
*
* @param array|null $stores An array of stores to get warnings for, or null for all.
* @return string[]
*/
public static function warnings(array $stores = null) {
global $CFG;
if ($stores === null) {
require_once($CFG->dirroot.'/cache/locallib.php');
$stores = core_cache\administration_helper::get_store_instance_summaries();
}
$warnings = array();
foreach ($stores as $store) {
if (!empty($store['warnings'])) {
$warnings = array_merge($warnings, $store['warnings']);
}
}
return $warnings;
}
/**
* A helper to determine whether a result was found.
*
* This has been deemed required after people have been confused by the fact that [] == false.
*
* @param mixed $value
* @return bool
*/
public static function result_found($value): bool {
return $value !== false;
}
/**
* Checks whether the cluster mode is available in PHP.
*
* @return bool Return true if the PHP supports redis cluster, otherwise false.
*/
public static function is_cluster_available(): bool {
return class_exists('RedisCluster');
}
}
+605
View File
@@ -0,0 +1,605 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 the value and actual version for the given key, with at least the required version.
*
* If there is no value for the key, or there is a value but it doesn't have the required
* version, then this function will return false (or throw an exception if you set strictness
* to MUST_EXIST).
*
* This function can be used to make it easier to support localisable caches (where the cache
* could be stored on a local server as well as a shared cache). Specifying the version means
* that it will automatically retrieve the correct version if available, either from the local
* server or [if that has an older version] from the shared server.
*
* If the cached version is newer than specified version, it will be returned regardless. For
* example, if you request version 4, but the locally cached version is 5, it will be returned.
* If you request version 6, and the locally cached version is 5, then the system will look in
* higher-level caches (if any); if there still isn't a version 6 or greater, it will return
* null.
*
* You must use this function if you use set_versioned.
*
* @param string|int $key The key for the data being requested.
* @param int $requiredversion Minimum required version of the data
* @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
* @return mixed Data from the cache, or false if the key did not exist or was too old
*/
public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null);
/**
* 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.
*
* <code>
* // 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');
* </code>
*
* @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);
/**
* Sets the value for the given key with the given version.
*
* The cache does not store multiple versions - any existing version will be overwritten with
* this one. This function should only be used if there is a known 'current version' (e.g.
* stored in a database table). It only ensures that the cache does not return outdated data.
*
* This function can be used to help implement localisable caches (where the cache could be
* stored on a local server as well as a shared cache). The version will be recorded alongside
* the item and get_versioned will always return the correct version.
*
* The version number must be an integer that always increases. This could be based on the
* current time, or a stored value that increases by 1 each time it changes, etc.
*
* If you use this function you must use get_versioned to retrieve the data.
*
* @param string|int $key The key for the data being set.
* @param int $version Integer for the version of the data
* @param mixed $data The data to set against the key.
* @return bool True on success, false otherwise.
*/
public function set_versioned($key, int $version, $data): bool;
/**
* 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.
*
* <code>
* // 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'
* ));
* </code>
*
* @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:
* <ol>
* <li>Attempt to retrieve the information.</li>
* <li>Generate the information.</li>
* <li>Attempt to set the information</li>
* </ol>
*
* 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 responsibility of the calling code to ensure
* locks are acquired, checked, and released.
*
* Prior to Moodle 4,3 this function used to return false if the lock cannot be obtained. It
* now always returns true, and throws an exception if the lock cannot be obtained.
*
* @param string|int $key
* @return bool Always returns true (for backwards compatibility)
* @throws moodle_exception If the lock cannot be obtained after a timeout
*/
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 responsibility 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 check_lock_state($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 responsibility 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 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 $ownerid The identifier so we can check if we have the lock or if it is someone else.
* The use of this property is entirely optional and implementations can act as they like upon it.
* @return bool True if the lock could be acquired, false otherwise.
*/
public function acquire_lock($key, $ownerid);
/**
* Test if there is already a lock for the given key and if there is whether it belongs to the calling code.
*
* @param string $key The key we are locking.
* @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
* @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
* is no lock.
*/
public function check_lock_state($key, $ownerid);
/**
* Releases the lock on the given key.
*
* @param string $key The key we are locking.
* @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
* The use of this property is entirely optional and implementations can act as they like upon it.
* @return bool True if the lock has been released, false if there was a problem releasing the lock.
*/
public function release_lock($key, $ownerid);
}
/**
* 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:
* <ol>
* <li>Attempt to retrieve the information.</li>
* <li>Generate the information.</li>
* <li>Attempt to set the information</li>
* </ol>
*
* 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 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 Moodle 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.
*
* This feature should be implemented by all cache stores that are configurable when adding an instance.
* It requires the implementation of methods required to convert form data into the a configuration array for the
* store instance, and then the reverse converting configuration data into an array that can be used to set the
* data for the edit form.
*
* Can be implemented by classes already implementing cache_store.
*/
interface cache_is_configurable {
/**
* Given the data from the add instance form this function creates a configuration array.
*
* @param stdClass $data
* @return array
*/
public static function config_get_configuration_array($data);
/**
* Allows the cache store to set its data against the edit form before it is shown to the user.
*
* @param moodleform $editform
* @param array $config
*/
public static function config_set_edit_form_data(moodleform $editform, array $config);
}
/**
* 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);
}
/**
* Versionable cache data source.
*
* This interface extends the main cache data source interface to add an extra required method if
* the data source is to be used for a versioned cache.
*
* @package core_cache
*/
interface cache_data_source_versionable extends cache_data_source {
/**
* Loads the data for the key provided ready formatted for caching.
*
* If there is no data for that key, or if the data for the required key has an older version
* than the specified $requiredversion, then this returns null.
*
* If there is data then $actualversion should be set to the actual version number retrieved
* (may be the same as $requiredversion or newer).
*
* @param string|int $key The key to load.
* @param int $requiredversion Minimum required version
* @param mixed $actualversion Should be set to the actual version number retrieved
* @return mixed What ever data should be returned, or false if it can't be loaded.
*/
public function load_for_cache_versioned($key, int $requiredversion, &$actualversion);
}
/**
* 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);
}
/**
* Cache lock interface
*
* This interface needs to be inherited by all cache lock plugins.
*/
interface cache_lock_interface {
/**
* Constructs an instance of the cache lock given its name and its configuration data
*
* @param string $name The unique name of the lock instance
* @param array $configuration
*/
public function __construct($name, array $configuration = array());
/**
* Acquires a lock on a given key.
*
* @param string $key The key to acquire a lock for.
* @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
* to use this. Each implementation can decide for themselves.
* @param bool $block If set to true the application will wait until a lock can be acquired
* @return bool True if the lock can be acquired false otherwise.
*/
public function lock($key, $ownerid, $block = false);
/**
* Releases the lock held on a certain key.
*
* @param string $key The key to release the lock for.
* @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
* to use this. Each implementation can decide for themselves.
* @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
*/
public function unlock($key, $ownerid, $forceunlock = false);
/**
* Checks the state of the given key.
*
* Returns true if the key is locked and belongs to the ownerid.
* Returns false if the key is locked but does not belong to the ownerid.
* Returns null if there is no lock
*
* @param string $key The key we are checking for.
* @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
* @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
* is no lock.
*/
public function check_state($key, $ownerid);
/**
* Cleans up any left over locks.
*
* This function MUST clean up any locks that have been acquired and not released during processing.
* Although the situation of acquiring a lock and not releasing it should be insanely rare we need to deal with it.
* Things such as unfortunate timeouts etc could cause this situation.
*/
public function __destruct();
}
+2461
View File
File diff suppressed because it is too large Load Diff
+889
View File
@@ -0,0 +1,889 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache display administration helper.
*
* 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
* @author Peter Burnett <peterburnett@catalyst-au.net>
* @copyright 2020 Catalyst IT
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_cache\local;
use cache_store, cache_factory, cache_config_writer, cache_helper;
use core\output\notification;
/**
* A cache helper for administration tasks
*
* @package core
* @category cache
* @copyright 2020 Peter Burnett <peterburnett@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class administration_display_helper extends \core_cache\administration_helper {
/**
* Please do not call constructor directly. Use cache_factory::get_administration_display_helper() instead.
*/
public function __construct() {
// Nothing to do here.
}
/**
* Returns all of the actions that can be performed on a definition.
*
* @param context $context the system context.
* @param array $definitionsummary information about this cache, from the array returned by
* core_cache\administration_helper::get_definition_summaries(). Currently only 'sharingoptions'
* element is used.
* @return array of actions. Each action is an action_url.
*/
public function get_definition_actions(\context $context, array $definitionsummary): array {
global $OUTPUT;
if (has_capability('moodle/site:config', $context)) {
$actions = array();
// Edit mappings.
$actions[] = $OUTPUT->action_link(
new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping',
'definition' => $definitionsummary['id'])),
get_string('editmappings', 'cache')
);
// Edit sharing.
if (count($definitionsummary['sharingoptions']) > 1) {
$actions[] = $OUTPUT->action_link(
new \moodle_url('/cache/admin.php', array('action' => 'editdefinitionsharing',
'definition' => $definitionsummary['id'])),
get_string('editsharing', 'cache')
);
}
// Purge.
$actions[] = $OUTPUT->action_link(
new \moodle_url('/cache/admin.php', array('action' => 'purgedefinition',
'definition' => $definitionsummary['id'], 'sesskey' => sesskey())),
get_string('purge', 'cache')
);
return $actions;
}
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 information about this store, from the array returned by
* core_cache\administration_helper::get_store_instance_summaries().
* @return array of actions. Each action is an action_url.
*/
public function get_store_instance_actions(string $name, array $storedetails): array {
global $OUTPUT;
$actions = array();
if (has_capability('moodle/site:config', \context_system::instance())) {
$baseurl = new \moodle_url('/cache/admin.php', array('store' => $name));
if (empty($storedetails['default'])) {
// Edit store.
$actions[] = $OUTPUT->action_link(
new \moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin'])),
get_string('editstore', 'cache')
);
// Delete store.
$actions[] = $OUTPUT->action_link(
new \moodle_url($baseurl, array('action' => 'deletestore')),
get_string('deletestore', 'cache')
);
}
// Purge store.
$actions[] = $OUTPUT->action_link(
new \moodle_url($baseurl, array('action' => 'purgestore', 'sesskey' => sesskey())),
get_string('purge', 'cache')
);
}
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 information about this store, from the array returned by
* core_cache\administration_helper::get_store_plugin_summaries().
* @return array of actions. Each action is an action_url.
*/
public function get_store_plugin_actions(string $name, array $plugindetails): array {
global $OUTPUT;
$actions = array();
if (has_capability('moodle/site:config', \context_system::instance())) {
if (!empty($plugindetails['canaddinstance'])) {
$url = new \moodle_url('/cache/admin.php',
array('action' => 'addstore', 'plugin' => $name));
$actions[] = $OUTPUT->action_link(
$url,
get_string('addinstance', 'cache')
);
}
}
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 cachestore_addinstance_form
* @throws coding_exception
*/
public function get_add_store_form(string $plugin): \cachestore_addinstance_form {
global $CFG; // Needed for includes.
$plugins = \core_component::get_plugin_list('cachestore');
if (!array_key_exists($plugin, $plugins)) {
throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
}
$plugindir = $plugins[$plugin];
$class = 'cachestore_addinstance_form';
if (file_exists($plugindir.'/addinstanceform.php')) {
require_once($plugindir.'/addinstanceform.php');
if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
$class = 'cachestore_'.$plugin.'_addinstance_form';
if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
}
}
}
$locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
$url = new \moodle_url('/cache/admin.php', array('action' => 'addstore'));
return new $class($url, array('plugin' => $plugin, 'store' => null, 'locks' => $locks));
}
/**
* Returns a form that can be used to edit a store instance.
*
* @param string $plugin
* @param string $store
* @return cachestore_addinstance_form
* @throws coding_exception
*/
public function get_edit_store_form(string $plugin, string $store): \cachestore_addinstance_form {
global $CFG; // Needed for includes.
$plugins = \core_component::get_plugin_list('cachestore');
if (!array_key_exists($plugin, $plugins)) {
throw new \coding_exception('Invalid cache plugin used when trying to create an edit form.');
}
$factory = \cache_factory::instance();
$config = $factory->create_config_instance();
$stores = $config->get_all_stores();
if (!array_key_exists($store, $stores)) {
throw new \coding_exception('Invalid store name given when trying to create an edit form.');
}
$plugindir = $plugins[$plugin];
$class = 'cachestore_addinstance_form';
if (file_exists($plugindir.'/addinstanceform.php')) {
require_once($plugindir.'/addinstanceform.php');
if (class_exists('cachestore_'.$plugin.'_addinstance_form')) {
$class = 'cachestore_'.$plugin.'_addinstance_form';
if (!array_key_exists('cachestore_addinstance_form', class_parents($class))) {
throw new \coding_exception('Cache plugin add instance forms must extend cachestore_addinstance_form');
}
}
}
$locks = $this->get_possible_locks_for_stores($plugindir, $plugin);
$url = new \moodle_url('/cache/admin.php', array('action' => 'editstore', 'plugin' => $plugin, 'store' => $store));
$editform = new $class($url, array('plugin' => $plugin, 'store' => $store, 'locks' => $locks));
if (isset($stores[$store]['lock'])) {
$editform->set_data(array('lock' => $stores[$store]['lock']));
}
// See if the cachestore is going to want to load data for the form.
// If it has a customised add instance form then it is going to want to.
$storeclass = 'cachestore_'.$plugin;
$storedata = $stores[$store];
if (array_key_exists('configuration', $storedata) &&
array_key_exists('cache_is_configurable', class_implements($storeclass))) {
$storeclass::config_set_edit_form_data($editform, $storedata['configuration']);
}
return $editform;
}
/**
* Returns an array of suitable lock instances for use with this plugin, or false if the plugin handles locking itself.
*
* @param string $plugindir
* @param string $plugin
* @return array|false
*/
protected function get_possible_locks_for_stores(string $plugindir, string $plugin) {
global $CFG; // Needed for includes.
$supportsnativelocking = false;
if (file_exists($plugindir.'/lib.php')) {
require_once($plugindir.'/lib.php');
$pluginclass = 'cachestore_'.$plugin;
if (class_exists($pluginclass)) {
$supportsnativelocking = array_key_exists('cache_is_lockable', class_implements($pluginclass));
}
}
if (!$supportsnativelocking) {
$config = \cache_config::instance();
$locks = array();
foreach ($config->get_locks() as $lock => $conf) {
if (!empty($conf['default'])) {
$name = get_string($lock, 'cache');
} else {
$name = $lock;
}
$locks[$lock] = $name;
}
} else {
$locks = false;
}
return $locks;
}
/**
* 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 function get_store_configuration_from_data(\stdClass $data): array {
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 = 'cachestore_'.$data->plugin;
if (!class_exists($class)) {
throw new \coding_exception('Invalid cache plugin provided.');
}
if (array_key_exists('cache_is_configurable', class_implements($class))) {
return $class::config_get_configuration_array($data);
}
return array();
}
/**
* Returns an array of lock plugins for which we can add an instance.
*
* Suitable for use within an mform select element.
*
* @return array
*/
public function get_addable_lock_options(): array {
$plugins = \core_component::get_plugin_list_with_class('cachelock', '', 'lib.php');
$options = array();
$len = strlen('cachelock_');
foreach ($plugins as $plugin => $class) {
$method = "$class::can_add_instance";
if (is_callable($method) && !call_user_func($method)) {
// Can't add an instance of this plugin.
continue;
}
$options[substr($plugin, $len)] = get_string('pluginname', $plugin);
}
return $options;
}
/**
* Gets the form to use when adding a lock instance.
*
* @param string $plugin
* @param array $lockplugin
* @return cache_lock_form
* @throws coding_exception
*/
public function get_add_lock_form(string $plugin, array $lockplugin = null): \cache_lock_form {
global $CFG; // Needed for includes.
$plugins = \core_component::get_plugin_list('cachelock');
if (!array_key_exists($plugin, $plugins)) {
throw new \coding_exception('Invalid cache lock plugin requested when trying to create a form.');
}
$plugindir = $plugins[$plugin];
$class = 'cache_lock_form';
if (file_exists($plugindir.'/addinstanceform.php') && in_array('cache_is_configurable', class_implements($class))) {
require_once($plugindir.'/addinstanceform.php');
if (class_exists('cachelock_'.$plugin.'_addinstance_form')) {
$class = 'cachelock_'.$plugin.'_addinstance_form';
if (!array_key_exists('cache_lock_form', class_parents($class))) {
throw new \coding_exception('Cache lock plugin add instance forms must extend cache_lock_form');
}
}
}
return new $class(null, array('lock' => $plugin));
}
/**
* Gets configuration data from a new lock instance form.
*
* @param string $plugin
* @param stdClass $data
* @return array
* @throws coding_exception
*/
public function get_lock_configuration_from_data(string $plugin, \stdClass $data): array {
global $CFG;
$file = $CFG->dirroot.'/cache/locks/'.$plugin.'/lib.php';
if (!file_exists($file)) {
throw new \coding_exception('Invalid cache plugin provided. '.$file);
}
require_once($file);
$class = 'cachelock_'.$plugin;
if (!class_exists($class)) {
throw new \coding_exception('Invalid cache plugin provided.');
}
if (array_key_exists('cache_is_configurable', class_implements($class))) {
return $class::config_get_configuration_array($data);
}
return array();
}
/**
* Handles the page actions, based on the parameter.
*
* @param string $action the action to handle.
* @param array $forminfo an empty array to be overridden and set.
* @return array the empty or overridden forminfo array.
*/
public function perform_cache_actions(string $action, array $forminfo): array {
switch ($action) {
case 'rescandefinitions' : // Rescan definitions.
$this->action_rescan_definition();
break;
case 'addstore' : // Add the requested store.
$forminfo = $this->action_addstore();
break;
case 'editstore' : // Edit the requested store.
$forminfo = $this->action_editstore();
break;
case 'deletestore' : // Delete a given store.
$this->action_deletestore($action);
break;
case 'editdefinitionmapping' : // Edit definition mappings.
$forminfo = $this->action_editdefinitionmapping();
break;
case 'editdefinitionsharing' : // Edit definition sharing.
$forminfo = $this->action_editdefinitionsharing();
break;
case 'editmodemappings': // Edit default mode mappings.
$forminfo = $this->action_editmodemappings();
break;
case 'purgedefinition': // Purge a specific definition.
$this->action_purgedefinition();
break;
case 'purgestore':
case 'purge': // Purge a store cache.
$this->action_purge();
break;
case 'newlockinstance':
$forminfo = $this->action_newlockinstance();
break;
case 'deletelock':
// Deletes a lock instance.
$this->action_deletelock($action);
break;
}
return $forminfo;
}
/**
* Performs the rescan definition action.
*
* @return void
*/
public function action_rescan_definition() {
global $PAGE;
require_sesskey();
\cache_config_writer::update_definitions();
redirect($PAGE->url);
}
/**
* Performs the add store action.
*
* @return array an array of the form to display to the user, and the page title.
*/
public function action_addstore(): array {
global $PAGE;
$storepluginsummaries = $this->get_store_plugin_summaries();
$plugin = required_param('plugin', PARAM_PLUGIN);
if (!$storepluginsummaries[$plugin]['canaddinstance']) {
throw new \moodle_exception('ex_unmetstorerequirements', 'cache');
}
$mform = $this->get_add_store_form($plugin);
$title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
$config = $this->get_store_configuration_from_data($data);
$writer = \cache_config_writer::instance();
unset($config['lock']);
foreach ($writer->get_locks() as $lock => $lockconfig) {
if ($lock == $data->lock) {
$config['lock'] = $data->lock;
}
}
$writer->add_store_instance($data->name, $data->plugin, $config);
redirect($PAGE->url, get_string('addstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
}
$PAGE->navbar->add(get_string('addstore', 'cache', 'cache'), $PAGE->url);
return array('form' => $mform, 'title' => $title);
}
/**
* Performs the edit store action.
*
* @return array an array of the form to display, and the page title.
*/
public function action_editstore(): array {
global $PAGE;
$storepluginsummaries = $this->get_store_plugin_summaries();
$plugin = required_param('plugin', PARAM_PLUGIN);
$store = required_param('store', PARAM_TEXT);
$mform = $this->get_edit_store_form($plugin, $store);
$title = get_string('addstore', 'cache', $storepluginsummaries[$plugin]['name']);
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
$config = $this->get_store_configuration_from_data($data);
$writer = \cache_config_writer::instance();
unset($config['lock']);
foreach ($writer->get_locks() as $lock => $lockconfig) {
if ($lock == $data->lock) {
$config['lock'] = $data->lock;
}
}
$writer->edit_store_instance($data->name, $data->plugin, $config);
redirect($PAGE->url, get_string('editstoresuccess', 'cache', $storepluginsummaries[$plugin]['name']), 5);
}
return array('form' => $mform, 'title' => $title);
}
/**
* Performs the deletestore action.
*
* @param string $action the action calling to this function.
*/
public function action_deletestore(string $action): void {
global $OUTPUT, $PAGE, $SITE;
$notifysuccess = true;
$storeinstancesummaries = $this->get_store_instance_summaries();
$store = required_param('store', PARAM_TEXT);
$confirm = optional_param('confirm', false, PARAM_BOOL);
if (!array_key_exists($store, $storeinstancesummaries)) {
$notifysuccess = false;
$notification = get_string('invalidstore', 'cache');
} else if ($storeinstancesummaries[$store]['mappings'] > 0) {
$notifysuccess = false;
$notification = get_string('deletestorehasmappings', 'cache');
}
if ($notifysuccess) {
if (!$confirm) {
$title = get_string('confirmstoredeletion', 'cache');
$params = array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
$url = new \moodle_url($PAGE->url, $params);
$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);
$confirmation = get_string('deletestoreconfirmation', 'cache', $storeinstancesummaries[$store]['name']);
echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
echo $OUTPUT->footer();
exit;
} else {
require_sesskey();
$writer = \cache_config_writer::instance();
$writer->delete_store_instance($store);
redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5);
}
} else {
redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR);
}
}
/**
* Performs the edit definition mapping action.
*
* @return array an array of the form to display, and the page title.
* @throws cache_exception
*/
public function action_editdefinitionmapping(): array {
global $PAGE;
$definitionsummaries = $this->get_definition_summaries();
$definition = required_param('definition', PARAM_SAFEPATH);
if (!array_key_exists($definition, $definitionsummaries)) {
throw new \cache_exception('Invalid cache definition requested');
}
$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);
}
$PAGE->navbar->add(get_string('updatedefinitionmapping', 'cache'), $PAGE->url);
return array('form' => $mform, 'title' => $title);
}
/**
* Performs the edit definition sharing action.
*
* @return array an array of the edit definition sharing form, and the page title.
*/
public function action_editdefinitionsharing(): array {
global $PAGE;
$definitionsummaries = $this->get_definition_summaries();
$definition = required_param('definition', PARAM_SAFEPATH);
if (!array_key_exists($definition, $definitionsummaries)) {
throw new \cache_exception('Invalid cache definition requested');
}
$title = get_string('editdefinitionsharing', 'cache', $definition);
$sharingoptions = $definitionsummaries[$definition]['sharingoptions'];
$customdata = array('definition' => $definition, 'sharingoptions' => $sharingoptions);
$mform = new \cache_definition_sharing_form($PAGE->url, $customdata);
$mform->set_data(array(
'sharing' => $definitionsummaries[$definition]['selectedsharingoption'],
'userinputsharingkey' => $definitionsummaries[$definition]['userinputsharingkey']
));
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
$component = $definitionsummaries[$definition]['component'];
$area = $definitionsummaries[$definition]['area'];
// Purge the stores removing stale data before we alter the sharing option.
\cache_helper::purge_stores_used_by_definition($component, $area);
$writer = \cache_config_writer::instance();
$sharing = array_sum(array_keys($data->sharing));
$userinputsharingkey = $data->userinputsharingkey;
$writer->set_definition_sharing($definition, $sharing, $userinputsharingkey);
redirect($PAGE->url);
}
$PAGE->navbar->add(get_string('updatedefinitionsharing', 'cache'), $PAGE->url);
return array('form' => $mform, 'title' => $title);
}
/**
* Performs the edit mode mappings action.
*
* @return array an array of the edit mode mappings form.
*/
public function action_editmodemappings(): array {
global $PAGE;
$storeinstancesummaries = $this->get_store_instance_summaries();
$defaultmodestores = $this->get_default_mode_stores();
$mform = new \cache_mode_mappings_form(null, $storeinstancesummaries);
$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);
}
return array('form' => $mform);
}
/**
* Performs the purge definition action.
*
* @return void
*/
public function action_purgedefinition() {
global $PAGE;
require_sesskey();
$id = required_param('definition', PARAM_SAFEPATH);
list($component, $area) = explode('/', $id, 2);
$factory = cache_factory::instance();
$definition = $factory->create_definition($component, $area);
if ($definition->has_required_identifiers()) {
// We will have to purge the stores used by this definition.
cache_helper::purge_stores_used_by_definition($component, $area);
} else {
// Alrighty we can purge just the data belonging to this definition.
cache_helper::purge_by_definition($component, $area);
}
$message = get_string('purgexdefinitionsuccess', 'cache', [
'name' => $definition->get_name(),
'component' => $component,
'area' => $area,
]);
$purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
'action' => 'purgedefinition', 'sesskey' => sesskey(), 'definition' => $id]),
get_string('purgeagain', 'cache'));
redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
}
/**
* Performs the purge action.
*
* @return void
*/
public function action_purge() {
global $PAGE;
require_sesskey();
$store = required_param('store', PARAM_TEXT);
cache_helper::purge_store($store);
$message = get_string('purgexstoresuccess', 'cache', ['store' => $store]);
$purgeagainlink = \html_writer::link(new \moodle_url('/cache/admin.php', [
'action' => 'purgestore', 'sesskey' => sesskey(), 'store' => $store]),
get_string('purgeagain', 'cache'));
redirect($PAGE->url, $message . ' ' . $purgeagainlink, 5);
}
/**
* Performs the new lock instance action.
*
* @return array An array containing the new lock instance form.
*/
public function action_newlockinstance(): array {
global $PAGE;
// Adds a new lock instance.
$lock = required_param('lock', PARAM_ALPHANUMEXT);
$mform = $this->get_add_lock_form($lock);
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
$factory = cache_factory::instance();
$config = $factory->create_config_instance(true);
$name = $data->name;
$data = $this->get_lock_configuration_from_data($lock, $data);
$config->add_lock_instance($name, $lock, $data);
redirect($PAGE->url, get_string('addlocksuccess', 'cache', $name), 5);
}
return array('form' => $mform);
}
/**
* Performs the delete lock action.
*
* @param string $action the action calling this function.
*/
public function action_deletelock(string $action): void {
global $OUTPUT, $PAGE, $SITE;
$notifysuccess = true;
$locks = $this->get_lock_summaries();
$lock = required_param('lock', PARAM_ALPHANUMEXT);
$confirm = optional_param('confirm', false, PARAM_BOOL);
if (!array_key_exists($lock, $locks)) {
$notifysuccess = false;
$notification = get_string('invalidlock', 'cache');
} else if ($locks[$lock]['uses'] > 0) {
$notifysuccess = false;
$notification = get_string('deletelockhasuses', 'cache');
}
if ($notifysuccess) {
if (!$confirm) {
$title = get_string('confirmlockdeletion', 'cache');
$params = array('lock' => $lock, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey());
$url = new \moodle_url($PAGE->url, $params);
$button = new \single_button($url, get_string('deletelock', 'cache'));
$PAGE->set_title($title);
$PAGE->set_heading($SITE->fullname);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
$confirmation = get_string('deletelockconfirmation', 'cache', $lock);
echo $OUTPUT->confirm($confirmation, $button, $PAGE->url);
echo $OUTPUT->footer();
exit;
} else {
require_sesskey();
$writer = cache_config_writer::instance();
$writer->delete_lock_instance($lock);
redirect($PAGE->url, get_string('deletelocksuccess', 'cache'), 5);
}
} else {
redirect($PAGE->url, $notification, null, notification::NOTIFY_ERROR);
}
}
/**
* Outputs the main admin page by generating it through the renderer.
*
* @param \core_cache\output\renderer $renderer the renderer to use to generate the page.
* @return string the HTML for the admin page.
*/
public function generate_admin_page(\core_cache\output\renderer $renderer): string {
$context = \context_system::instance();
$html = '';
$storepluginsummaries = $this->get_store_plugin_summaries();
$storeinstancesummaries = $this->get_store_instance_summaries();
$definitionsummaries = $this->get_definition_summaries();
$defaultmodestores = $this->get_default_mode_stores();
$locks = $this->get_lock_summaries();
$html .= $renderer->store_plugin_summaries($storepluginsummaries);
$html .= $renderer->store_instance_summariers($storeinstancesummaries, $storepluginsummaries);
$html .= $renderer->definition_summaries($definitionsummaries, $context);
$html .= $renderer->lock_summaries($locks);
$html .= $renderer->additional_lock_actions();
$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'));
$html .= $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl);
return $html;
}
/**
* Gets usage information about the whole cache system.
*
* This is a slow function and should only be used on an admin information page.
*
* The returned array lists all cache definitions with fields 'cacheid' and 'stores'. For
* each store, the following fields are available:
*
* - name (store name)
* - class (e.g. cachestore_redis)
* - supported (true if we have any information)
* - items (number of items stored)
* - mean (mean size of item)
* - sd (standard deviation for item sizes)
* - margin (margin of error for mean at 95% confidence)
* - storetotal (total usage for store if known, otherwise null)
*
* The storetotal field will be the same for every cache that uses the same store.
*
* @param int $samplekeys Number of keys to sample when checking size of large caches
* @return array Details of cache usage
*/
public function get_usage(int $samplekeys): array {
$results = [];
$factory = cache_factory::instance();
// Check the caches we already have an instance of, so we don't make another one...
$got = $factory->get_caches_in_use();
$gotid = [];
foreach ($got as $longid => $unused) {
// The IDs here can be of the form cacheid/morestuff if there are parameters in the
// cache. Any entry for a cacheid is good enough to consider that we don't need to make
// another entry ourselves, so we remove the extra bits and track the basic cache id.
$gotid[preg_replace('~^([^/]+/[^/]+)/.*$~', '$1', $longid)] = true;
}
$storetotals = [];
$config = $factory->create_config_instance();
foreach ($config->get_definitions() as $configdetails) {
if (!array_key_exists($configdetails['component'] . '/' . $configdetails['area'], $gotid)) {
// Where possible (if it doesn't need identifiers), make an instance of the cache, otherwise
// we can't get the store instances for it (and it won't show up in the list).
if (empty($configdetails['requireidentifiers'])) {
\cache::make($configdetails['component'], $configdetails['area']);
}
}
$definition = $factory->create_definition($configdetails['component'], $configdetails['area']);
$stores = $factory->get_store_instances_in_use($definition);
// Create object for results about this cache definition.
$currentresult = (object)['cacheid' => $definition->get_id(), 'stores' => []];
$results[$currentresult->cacheid] = $currentresult;
/** @var cache_store $store */
foreach ($stores as $store) {
// Skip static cache.
if ($store instanceof \cachestore_static) {
continue;
}
// Get cache size details from store.
$currentstore = $store->cache_size_details($samplekeys);
// Add in basic information about store.
$currentstore->name = $store->my_name();
$currentstore->class = get_class($store);
// Add in store total.
if (!array_key_exists($currentstore->name, $storetotals)) {
$storetotals[$currentstore->name] = $store->store_total_size();
}
$currentstore->storetotal = $storetotals[$currentstore->name];
$currentresult->stores[] = $currentstore;
}
}
ksort($results);
return $results;
}
}
+550
View File
@@ -0,0 +1,550 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache\output;
use cache_factory;
use cache_store;
use context;
use core_collator;
use html_table;
use html_table_cell;
use html_table_row;
use html_writer;
use lang_string;
use moodle_url;
use single_select;
/**
* The cache renderer (mainly admin interfaces).
*
* @package core_cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer extends \plugin_renderer_base {
/**
* Displays store summaries.
*
* @param array $storeinstancesummaries information about each store instance,
* as returned by core_cache\administration_helper::get_store_instance_summaries().
* @param array $storepluginsummaries information about each store plugin as
* returned by core_cache\administration_helper::get_store_plugin_summaries().
* @return string HTML
*/
public function store_instance_summariers(array $storeinstancesummaries, array $storepluginsummaries) {
$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('locking', 'cache') . ' ' . $this->output->help_icon('locking', 'cache'),
get_string('actions', 'cache'),
);
$table->colclasses = array(
'storename',
'plugin',
'storeready',
'mappings',
'modes',
'supports',
'locking',
'actions'
);
$table->data = array();
$defaultstoreactions = get_string('defaultstoreactions', 'cache');
foreach ($storeinstancesummaries as $name => $storesummary) {
$htmlactions = cache_factory::get_administration_display_helper()->get_store_instance_actions($name, $storesummary);
$modes = array();
foreach ($storesummary['modes'] as $mode => $enabled) {
if ($enabled) {
$modes[] = get_string('mode_'.$mode, 'cache');
}
}
$supports = array();
foreach ($storesummary['supports'] as $support => $enabled) {
if ($enabled) {
$supports[] = get_string('supports_'.$support, 'cache');
}
}
$info = '';
if (!empty($storesummary['default'])) {
$info = $this->output->pix_icon('i/info', $defaultstoreactions, '', array('class' => 'icon'));
}
$isready = $storesummary['isready'] && $storesummary['requirementsmet'];
$readycell = new html_table_cell;
if ($isready) {
$readycell->text = $this->output->pix_icon('i/valid', '1');
}
$storename = $storesummary['name'];
if (!empty($storesummary['default'])) {
$storename = get_string('store_'.$storesummary['name'], 'cache');
}
if (!$isready && (int)$storesummary['mappings'] > 0) {
$readycell->text = $this->output->help_icon('storerequiresattention', 'cache');
$readycell->attributes['class'] = 'store-requires-attention';
}
$lock = $storesummary['lock']['name'];
if (!empty($storesummary['lock']['default'])) {
$lock = get_string($storesummary['lock']['name'], 'cache');
}
$row = new html_table_row(array(
$storename,
get_string('pluginname', 'cachestore_'.$storesummary['plugin']),
$readycell,
$storesummary['mappings'],
join(', ', $modes),
join(', ', $supports),
$lock,
$info.join(', ', $htmlactions)
));
$row->attributes['class'] = 'store-'.$name;
if ($storesummary['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 $storepluginsummaries information about each store plugin as
* returned by core_cache\administration_helper::get_store_plugin_summaries().
* @return string HTML
*/
public function store_plugin_summaries(array $storepluginsummaries) {
$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 ($storepluginsummaries as $name => $plugin) {
$htmlactions = cache_factory::get_administration_display_helper()->get_store_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');
}
}
$row = new html_table_row(array(
$plugin['name'],
($plugin['requirementsmet']) ? $this->output->pix_icon('i/valid', '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 $definitionsummaries information about each definition, as returned by
* core_cache\administration_helper::get_definition_summaries().
* @param context $context the system context.
*
* @return string HTML.
*/
public function definition_summaries(array $definitionsummaries, context $context) {
$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('sharing', 'cache'),
get_string('canuselocalstore', 'cache'),
get_string('actions', 'cache')
);
$table->colclasses = array(
'definition',
'mode',
'component',
'area',
'mappings',
'sharing',
'canuselocalstore',
'actions'
);
$table->data = array();
core_collator::asort_array_of_arrays_by_key($definitionsummaries, 'name');
$none = new lang_string('none', 'cache');
foreach ($definitionsummaries as $id => $definition) {
$htmlactions = cache_factory::get_administration_display_helper()->get_definition_actions($context, $definition);
if (!empty($definition['mappings'])) {
$mapping = join(', ', $definition['mappings']);
} else {
$mapping = '<em>'.$none.'</em>';
}
$uselocalcachecol = get_string('no');
if ($definition['mode'] != cache_store::MODE_REQUEST) {
if (isset($definition['canuselocalstore']) && $definition['canuselocalstore']) {
$uselocalcachecol = get_string('yes');
}
}
$row = new html_table_row(array(
$definition['name'],
get_string('mode_'.$definition['mode'], 'cache'),
$definition['component'],
$definition['area'],
$mapping,
join(', ', $definition['selectedsharingoption']),
$uselocalcachecol,
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);
$url = new moodle_url('/cache/admin.php', array('action' => 'rescandefinitions', 'sesskey' => sesskey()));
$link = html_writer::link($url, get_string('rescandefinitions', 'cache'));
$html .= html_writer::tag('div', $link, 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);
$link = html_writer::link($editurl, get_string('editmappings', 'cache'));
$html .= html_writer::tag('div', $link, array('class' => 'edit-link'));
$html .= html_writer::end_tag('div');
return $html;
}
/**
* Display basic information about lock instances.
*
* @todo Add some actions so that people can configure lock instances.
*
* @param array $locks
* @return string
*/
public function lock_summaries(array $locks) {
$table = new html_table();
$table->colclasses = array(
'name',
'type',
'default',
'uses',
'actions'
);
$table->rowclasses = array(
'lock_name',
'lock_type',
'lock_default',
'lock_uses',
'lock_actions',
);
$table->head = array(
get_string('lockname', 'cache'),
get_string('locktype', 'cache'),
get_string('lockdefault', 'cache'),
get_string('lockuses', 'cache'),
get_string('actions', 'cache')
);
$table->data = array();
$tick = $this->output->pix_icon('i/valid', '');
foreach ($locks as $lock) {
$actions = array();
if ($lock['uses'] === 0 && !$lock['default']) {
$url = new moodle_url('/cache/admin.php', array('lock' => $lock['name'], 'action' => 'deletelock'));
$actions[] = html_writer::link($url, get_string('delete', 'cache'));
}
$table->data[] = new html_table_row(array(
new html_table_cell($lock['name']),
new html_table_cell($lock['type']),
new html_table_cell($lock['default'] ? $tick : ''),
new html_table_cell($lock['uses']),
new html_table_cell(join(' ', $actions))
));
}
$html = html_writer::start_tag('div', array('id' => 'core-cache-lock-summary'));
$html .= $this->output->heading(get_string('locksummary', 'cache'), 3);
$html .= html_writer::table($table);
$html .= html_writer::end_tag('div');
return $html;
}
/**
* Renders additional actions for locks, such as Add.
*
* @return string
*/
public function additional_lock_actions(): string {
$url = new moodle_url('/cache/admin.php', array('action' => 'newlockinstance'));
$select = new single_select($url, 'lock', cache_factory::get_administration_display_helper()->get_addable_lock_options());
$select->label = get_string('addnewlockinstance', 'cache');
$html = html_writer::start_tag('div', array('id' => 'core-cache-lock-additional-actions'));
$html .= html_writer::tag('div', $this->output->render($select), array('class' => 'new-instance'));
$html .= html_writer::end_tag('div');
return $html;
}
/**
* Renders an array of notifications for the cache configuration screen.
*
* Takes an array of notifications with the form:
* $notifications = array(
* array('This is a success message', true),
* array('This is a failure message', false),
* );
*
* @param array $notifications
* @return string
*/
public function notifications(array $notifications = array()) {
if (count($notifications) === 0) {
// There are no notifications to render.
return '';
}
$html = html_writer::start_div('notifications');
foreach ($notifications as $notification) {
list($message, $notifysuccess) = $notification;
$html .= $this->notification($message, ($notifysuccess) ? 'notifysuccess' : 'notifyproblem');
}
$html .= html_writer::end_div();
return $html;
}
/**
* Creates the two tables which display on the usage page.
*
* @param array $usage Usage information (from cache_helper::usage)
* @return array Array of 2 tables (main and summary table)
* @throws \coding_exception
*/
public function usage_tables(array $usage): array {
$table = new \html_table();
$table->id = 'usage_main';
$table->head = [
get_string('definition', 'cache'),
get_string('storename', 'cache'),
get_string('plugin', 'cache'),
get_string('usage_items', 'cache'),
get_string('usage_mean', 'cache'),
get_string('usage_sd', 'cache'),
get_string('usage_total', 'cache'),
get_string('usage_totalmargin', 'cache')];
$table->align = [
'left', 'left', 'left',
'right', 'right', 'right', 'right', 'right'
];
$table->data = [];
$summarytable = new \html_table();
$summarytable->id = 'usage_summary';
$summarytable->head = [
get_string('storename', 'cache'),
get_string('plugin', 'cache'),
get_string('usage_total', 'cache'),
get_string('usage_realtotal', 'cache')
];
$summarytable->align = [
'left', 'left',
'right', 'right',
];
$summarytable->data = [];
$summarytable->attributes['class'] = 'generaltable w-auto';
$storetotals = [];
// We will highlight all cells that are more than 2% of total size, so work that out first.
$total = 0;
foreach ($usage as $definition) {
foreach ($definition->stores as $storedata) {
$total += $storedata->items * $storedata->mean;
}
}
$highlightover = round($total / 50);
foreach ($usage as $definition) {
foreach ($definition->stores as $storedata) {
$row = [];
$row[] = s($definition->cacheid);
$row[] = s($storedata->name);
$row[] = s($storedata->class);
if (!$storedata->supported) {
// We don't have data for this store because it isn't searchable.
$row[] = '-';
} else {
$row[] = $storedata->items;
}
if ($storedata->items) {
$row[] = display_size(round($storedata->mean));
if ($storedata->items > 1) {
$row[] = display_size(round($storedata->sd));
} else {
$row[] = '';
}
$cellsize = round($storedata->items * $storedata->mean);
$row[] = display_size($cellsize, 1, 'MB');
if (!array_key_exists($storedata->name, $storetotals)) {
$storetotals[$storedata->name] = (object)[
'plugin' => $storedata->class,
'total' => 0,
'storetotal' => $storedata->storetotal,
];
}
$storetotals[$storedata->name]->total += $cellsize;
} else {
$row[] = '';
$row[] = '';
$cellsize = 0;
$row[] = '';
}
if ($storedata->margin) {
// Plus or minus.
$row[] = '&#xb1;' . display_size($storedata->margin * $storedata->items, 1, 'MB');
} else {
$row[] = '';
}
$htmlrow = new \html_table_row($row);
if ($cellsize > $highlightover) {
$htmlrow->attributes = ['class' => 'table-warning'];
}
$table->data[] = $htmlrow;
}
}
ksort($storetotals);
foreach ($storetotals as $storename => $storedetails) {
$row = [s($storename), s($storedetails->plugin)];
$row[] = display_size($storedetails->total, 1, 'MB');
if ($storedetails->storetotal !== null) {
$row[] = display_size($storedetails->storetotal, 1, 'MB');
} else {
$row[] = '-';
}
$summarytable->data[] = $row;
}
return [$table, $summarytable];
}
/**
* Renders the usage page.
*
* @param \html_table $maintable Main table
* @param \html_table $summarytable Summary table
* @param \moodleform $samplesform Form to select number of samples
* @return string HTML for page
*/
public function usage_page(\html_table $maintable, \html_table $summarytable, \moodleform $samplesform): string {
$data = [
'maintable' => \html_writer::table($maintable),
'summarytable' => \html_writer::table($summarytable),
'samplesform' => $samplesform->render()
];
return $this->render_from_template('core_cache/usage', $data);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Form for usage page to select number of samples.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_cache\output;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Form for usage page to select number of samples.
*
* @package core_cache
*/
class usage_samples_form extends \moodleform {
/**
* Constructor sets form up to use GET request to current page.
*/
public function __construct() {
parent::__construct(null, null, 'get');
}
/**
* Adds controls to form.
*/
protected function definition() {
$mform = $this->_form;
$radioarray = [];
foreach ([50, 100, 200, 500, 1000] as $samples) {
$radioarray[] = $mform->createElement('radio', 'samples', '', $samples, $samples);
}
$mform->setDefault('samples', 50);
$mform->addGroup($radioarray, 'samplesradios', get_string('usage_samples', 'cache'), [' '], false);
$mform->addElement('submit', 'submit', get_string('update'));
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for core_cache.
*
* @package core_cache
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_cache\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
/**
* Privacy Subsystem implementation for core_cache.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// Caches store data.
\core_privacy\local\metadata\provider,
// The cache subsystem stores data on behalf of other components.
\core_privacy\local\request\subsystem\plugin_provider,
\core_privacy\local\request\shared_userlist_provider
{
/**
* Returns meta data about this system.
*
* Note, although this plugin does store user data, it is not able to
* identify it, and that user data is typically very short lived.
*
* Therefore it is not realistically possible to export any of this
* data as it is only identifiable by the plugin storing it, and that
* plugin should already be exporting the data as part of it's own
* implementation.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
// Data is stored in cache stores.
$collection->add_plugintype_link('cachestore', [], 'privacy:metadata:cachestore');
// Cache locks do not store any personal user data.
return $collection;
}
}
+536
View File
@@ -0,0 +1,536 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache store - base class
*
* This file is part of Moodle's cache API, affectionately called MUC.
* It contains the components that are required 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 store interface.
*
* This interface defines the static methods that must be implemented by every cache store plugin.
* To ensure plugins implement this class the abstract cache_store class implements this interface.
*
* @package core
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface cache_store_interface {
/**
* 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());
/**
* 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);
/**
* Generates the appropriate configuration required for unit testing.
*
* @return array Array of unit test configuration data to be used by initialise().
*/
public static function unit_test_configuration();
}
/**
* Abstract cache store class.
*
* All cache store plugins must extend this base class.
* It lays down the foundation for what is required of a cache store plugin.
*
* @since Moodle 2.4
* @package core
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class cache_store implements cache_store_interface {
// 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;
/**
* The cache is searchable by key.
*/
const IS_SEARCHABLE = 8;
/**
* The cache store dereferences objects.
*
* When set, loaders will assume that all data coming from this store has already had all references
* resolved. So even for complex object structures it will not try to remove references again.
*/
const DEREFERENCES_OBJECTS = 16;
// 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 caches.
*/
const STATIC_ACCEL = '** static accel. **';
/**
* Returned from get_last_io_bytes if this cache store doesn't support counting bytes read/sent.
*/
const IO_BYTES_NOT_SUPPORTED = -1;
/**
* Constructs an instance of the cache store.
*
* The constructor should be responsible for creating anything needed by the store that is not
* specific to a definition.
* Tasks such as opening a connection to check it is available are best done here.
* Tasks that are definition specific such as creating a storage area for the definition data
* or creating key tables and indexs are best done within the initialise method.
*
* Once a store has been constructed the cache API will check it is ready to be intialised with
* a definition by called $this->is_ready().
* If the setup of the store failed (connection could not be established for example) then
* that method should return false so that the store instance is not selected for use.
*
* @param string $name The name of the cache store
* @param array $configuration The configuration for this store instance.
*/
abstract public function __construct($name, array $configuration = array());
/**
* Returns the name of this store instance.
* @return string
*/
abstract public function my_name();
/**
* Initialises a new instance of the cache store given the definition the instance is to be used for.
*
* This function should be used to run any definition specific setup the store instance requires.
* Tasks such as creating storage areas, or creating indexes are best done here.
*
* Its important to note that the initialise method is expected to always succeed.
* If there are setup tasks that may fail they should be done within the __construct method
* and should they fail is_ready should return false.
*
* @param cache_definition $definition
*/
abstract public function initialise(cache_definition $definition);
/**
* Returns true if this cache store instance has been initialised.
* @return bool
*/
abstract public function is_initialised();
/**
* Returns true if this cache store instance is ready to use.
* @return bool
*/
public function is_ready() {
return forward_static_call(array($this, 'are_requirements_met'));
}
/**
* 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.
*/
abstract 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.
*/
abstract 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.
*/
abstract 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.
*/
abstract 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.
*/
abstract 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.
*/
abstract public function delete_many(array $keys);
/**
* Purges the cache deleting all items within it.
*
* @return boolean True on success. False otherwise.
*/
abstract public function purge();
/**
* @deprecated since 2.5
* @see \cache_store::instance_deleted()
*/
public function cleanup() {
throw new coding_exception('cache_store::cleanup() can not be used anymore.' .
' Please use cache_store::instance_deleted() instead.');
}
/**
* Performs any necessary operation when the store instance has been created.
*
* @since Moodle 2.5
*/
public function instance_created() {
// By default, do nothing.
}
/**
* Performs any necessary operation when the store instance is being deleted.
*
* This method may be called before the store has been initialised.
*
* @since Moodle 2.5
* @see cleanup()
*/
public function instance_deleted() {
if (method_exists($this, 'cleanup')) {
// There used to be a legacy function called cleanup, it was renamed to instance delete.
// To be removed in 2.6.
$this->cleanup();
}
}
/**
* Returns true if the user can add an instance of the store plugin.
*
* @return bool
*/
public static function can_add_instance() {
return true;
}
/**
* Returns true if the store instance guarantees data.
*
* @return bool
*/
public function supports_data_guarantee() {
return $this::get_supported_features() & self::SUPPORTS_DATA_GUARANTEE;
}
/**
* Returns true if the store instance supports multiple identifiers.
*
* @return bool
*/
public function supports_multiple_identifiers() {
return $this::get_supported_features() & self::SUPPORTS_MULTIPLE_IDENTIFIERS;
}
/**
* Returns true if the store instance supports native ttl.
*
* @return bool
*/
public function supports_native_ttl() {
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));
}
/**
* Returns true if the store automatically dereferences objects.
*
* @return bool
*/
public function supports_dereferencing_objects() {
return $this::get_supported_features() & self::DEREFERENCES_OBJECTS;
}
/**
* Creates a clone of this store instance ready to be initialised.
*
* This method is used so that a cache store needs only be constructed once.
* Future requests for an instance of the store will be given a cloned instance.
*
* If you are writing a cache store that isn't compatible with the clone operation
* you can override this method to handle any situations you want before cloning.
*
* @param array $details An array containing the details of the store from the cache config.
* @return cache_store
*/
public function create_clone(array $details = array()) {
// By default we just run clone.
// Any stores that have an issue with this will need to override the create_clone method.
return clone($this);
}
/**
* Can be overridden to return any warnings this store instance should make to the admin.
*
* This should be used to notify things like configuration conflicts etc.
* The warnings returned here will be displayed on the cache configuration screen.
*
* @return string[] An array of warning strings from the store instance.
*/
public function get_warnings() {
return array();
}
/**
* Estimates the storage size used within this cache if the given value is stored with the
* given key.
*
* This function is not exactly accurate; it does not necessarily take into account all the
* overheads involved. It is only intended to give a good idea of the relative size of
* different caches.
*
* The default implementation serializes both key and value and sums the lengths (as a rough
* estimate which is probably good enough for everything unless the cache offers compression).
*
* @param mixed $key Key
* @param mixed $value Value
* @return int Size in bytes
*/
public function estimate_stored_size($key, $value): int {
return strlen(serialize($key)) + strlen(serialize($value));
}
/**
* Gets the amount of memory/storage currently used by this cache store if known.
*
* This value should be obtained quickly from the store itself, if available.
*
* This is the total memory usage of the entire store, not for ther specific cache in question.
*
* Where not supported (default), will always return null.
*
* @return int|null Amount of memory used in bytes or null
*/
public function store_total_size(): ?int {
return null;
}
/**
* Gets the amount of memory used by this specific cache within the store, if known.
*
* This function may be slow and should not be called in normal usage, only for administration
* pages. The value is usually an estimate, and may not be available at all.
*
* When estimating, a number of sample items will be used for the estimate. If set to 50
* (default), then this function will retrieve 50 random items and use that to estimate the
* total size.
*
* The return value has the following fields:
* - supported (true if any other values are completed)
* - items (number of items)
* - mean (mean size of one item in bytes)
* - sd (standard deviation of item size in bytes, based on sample)
* - margin (95% confidence margin for mean - will be 0 if exactly computed)
*
* @param int $samplekeys Number of samples to use
* @return stdClass Object with information about the store size
*/
public function cache_size_details(int $samplekeys = 50): stdClass {
$result = (object)[
'supported' => false,
'items' => 0,
'mean' => 0,
'sd' => 0,
'margin' => 0
];
// If this cache isn't searchable, we don't know the answer.
if (!$this->is_searchable()) {
return $result;
}
$result->supported = true;
// Get all the keys for the cache.
$keys = $this->find_all();
$result->items = count($keys);
// Don't do anything else if there are no items.
if ($result->items === 0) {
return $result;
}
// Select N random keys.
$exact = false;
if ($result->items <= $samplekeys) {
$samples = $keys;
$exact = true;
} else {
$indexes = array_rand($keys, $samplekeys);
$samples = [];
foreach ($indexes as $index) {
$samples[] = $keys[$index];
}
}
// Get the random items from cache and estimate the size of each.
$sizes = [];
foreach ($samples as $samplekey) {
$value = $this->get($samplekey);
$sizes[] = $this->estimate_stored_size($samplekey, $value);
}
$number = count($sizes);
// Calculate the mean and standard deviation.
$result->mean = array_sum($sizes) / $number;
$squarediff = 0;
foreach ($sizes as $size) {
$squarediff += ($size - $result->mean) ** 2;
}
$squarediff /= $number;
$result->sd = sqrt($squarediff);
// If it's not exact, also calculate the confidence interval.
if (!$exact) {
// 95% confidence has a Z value of 1.96.
$result->margin = (1.96 * $result->sd) / sqrt($number);
}
return $result;
}
/**
* Returns true if this cache store instance is both suitable for testing, and ready for testing.
*
* Cache stores that support being used as the default store for unit and acceptance testing should
* override this function and return true if there requirements have been met.
*
* @return bool
*/
public static function ready_to_be_used_for_testing() {
return false;
}
/**
* Gets the number of bytes read from or written to cache as a result of the last action.
*
* This includes calls to the functions get(), get_many(), set(), and set_many(). The number
* is reset by calling any of these functions.
*
* This should be the actual number of bytes of the value read from or written to cache,
* giving an impression of the network or other load. It will not be exactly the same amount
* as netowrk traffic because of protocol overhead, key text, etc.
*
* If not supported, returns IO_BYTES_NOT_SUPPORTED.
*
* @return int Bytes read (or 0 if none/not supported)
* @since Moodle 4.0
*/
public function get_last_io_bytes(): int {
return self::IO_BYTES_NOT_SUPPORTED;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
/**
* Class wrapping information in the cache that is tagged with a version number.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class version_wrapper {
/**
* The data being stored.
* @var mixed
*/
public $data;
/**
* Version number for the data
* @var int
*/
public $version;
/**
* Constructs a version tag wrapper.
*
* @param mixed $data
* @param int $version Version number
*/
public function __construct($data, int $version) {
$this->data = $data;
$this->version = $version;
}
}
+608
View File
@@ -0,0 +1,608 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains classes that are used by the Cache API only when it is disabled.
*
* These classes are derivatives of other significant classes used by the Cache API customised specifically
* to only do what is absolutely necessary when initialising and using the Cache API when its been disabled.
*
* @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();
/**
* Required as it is needed for cache_config_disabled which extends cache_config_writer.
*/
require_once($CFG->dirroot.'/cache/locallib.php');
/**
* The cache loader class used when the Cache has been disabled.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_disabled extends cache implements cache_loader_with_locking {
/**
* Constructs the cache.
*
* @param cache_definition $definition
* @param cache_store $store
* @param null $loader Unused.
*/
public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
if ($loader instanceof cache_data_source) {
// Set the data source to allow data sources to work when caching is entirely disabled.
$this->set_data_source($loader);
}
// No other features are handled.
}
/**
* Gets a key from the cache.
*
* @param int|string $key
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
* @param int $strictness Unused.
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
* @return bool
*/
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
$datasource = $this->get_datasource();
if ($datasource !== false) {
if ($requiredversion === cache::VERSION_NONE) {
return $datasource->load_for_cache($key);
} else {
if (!$datasource instanceof cache_data_source_versionable) {
throw new \coding_exception('Data source is not versionable');
}
$result = $datasource->load_for_cache_versioned($key, $requiredversion, $actualversion);
if ($result && $actualversion < $requiredversion) {
throw new \coding_exception('Data source returned outdated version');
}
return $result;
}
}
return false;
}
/**
* Gets many keys at once from the cache.
*
* @param array $keys
* @param int $strictness Unused.
* @return array
*/
public function get_many(array $keys, $strictness = IGNORE_MISSING) {
if ($this->get_datasource() !== false) {
return $this->get_datasource()->load_many_for_cache($keys);
}
return array_combine($keys, array_fill(0, count($keys), false));
}
/**
* Sets a key value pair in the cache.
*
* @param int|string $key Unused.
* @param int $version Unused.
* @param mixed $data Unused.
* @param bool $setparents Unused.
* @return bool
*/
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
return false;
}
/**
* Sets many key value pairs in the cache at once.
*
* @param array $keyvaluearray Unused.
* @return int
*/
public function set_many(array $keyvaluearray) {
return 0;
}
/**
* Deletes an item from the cache.
*
* @param int|string $key Unused.
* @param bool $recurse Unused.
* @return bool
*/
public function delete($key, $recurse = true) {
return false;
}
/**
* Deletes many items at once from the cache.
*
* @param array $keys Unused.
* @param bool $recurse Unused.
* @return int
*/
public function delete_many(array $keys, $recurse = true) {
return 0;
}
/**
* Checks if the cache has the requested key.
*
* @param int|string $key Unused.
* @param bool $tryloadifpossible Unused.
* @return bool
*/
public function has($key, $tryloadifpossible = false) {
$result = $this->get($key);
return $result !== false;
}
/**
* Checks if the cache has all of the requested keys.
* @param array $keys Unused.
* @return bool
*/
public function has_all(array $keys) {
if (!$this->get_datasource()) {
return false;
}
foreach ($keys as $key) {
if (!$this->has($key)) {
return false;
}
}
return true;
}
/**
* Checks if the cache has any of the requested keys.
*
* @param array $keys Unused.
* @return bool
*/
public function has_any(array $keys) {
foreach ($keys as $key) {
if ($this->has($key)) {
return true;
}
}
return false;
}
/**
* Purges all items from the cache.
*
* @return bool
*/
public function purge() {
return true;
}
/**
* Pretend that we got a lock to avoid errors.
*
* @param int|string $key
* @return bool
*/
public function acquire_lock($key): bool {
return true;
}
/**
* Pretend that we released a lock to avoid errors.
*
* @param int|string $key
* @return bool
*/
public function release_lock($key): bool {
return true;
}
/**
* Pretend that we have a lock to avoid errors.
*
* @param int|string $key
* @return bool
*/
public function check_lock_state($key): bool {
return true;
}
}
/**
* The cache factory class used when the Cache has been disabled.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_factory_disabled extends cache_factory {
/** @var array Array of temporary caches in use. */
protected static $tempcaches = [];
/**
* Returns an instance of the cache_factor method.
*
* @param bool $forcereload Unused.
* @return cache_factory
* @throws coding_exception
*/
public static function instance($forcereload = false) {
throw new coding_exception('You must not call to this cache factory within your code.');
}
/**
* Creates a definition instance or returns the existing one if it has already been created.
*
* @param string $component
* @param string $area
* @param string $unused Used to be datasourceaggregate but that was removed and this is now unused.
* @return cache_definition
*/
public function create_definition($component, $area, $unused = null) {
$definition = parent::create_definition($component, $area);
if ($definition->has_data_source()) {
return $definition;
}
return cache_definition::load_adhoc(cache_store::MODE_REQUEST, $component, $area);
}
/**
* Common public method to create a cache instance given a definition.
*
* @param cache_definition $definition
* @return cache_application|cache_session|cache_store
* @throws coding_exception
*/
public function create_cache(cache_definition $definition) {
$loader = null;
if ($definition->has_data_source()) {
$loader = $definition->get_data_source();
}
return new cache_disabled($definition, $this->create_dummy_store($definition), $loader);
}
/**
* Creates a cache object given the parameters for a definition.
*
* @param string $component
* @param string $area
* @param array $identifiers
* @param string $unused Used to be datasourceaggregate but that was removed and this is now unused.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_definition($component, $area, array $identifiers = array(), $unused = null) {
// Temporary in-memory caches are sometimes allowed when caching is disabled.
if (\core_cache\allow_temporary_caches::is_allowed() && !$identifiers) {
$key = $component . '/' . $area;
if (array_key_exists($key, self::$tempcaches)) {
$cache = self::$tempcaches[$key];
} else {
$definition = $this->create_definition($component, $area);
// The cachestore_static class returns true to all three 'SUPPORTS_' checks so it
// can be used with all definitions.
$store = new cachestore_static('TEMP:' . $component . '/' . $area);
$store->initialise($definition);
// We need to use a cache loader wrapper rather than directly returning the store,
// or it wouldn't have support for versioning. The cache_application class is used
// (rather than cache_request which might make more sense logically) because it
// includes support for locking, which might be necessary for some caches.
$cache = new cache_application($definition, $store);
self::$tempcaches[$key] = $cache;
}
return $cache;
}
// Regular cache definitions are cached inside create_definition(). This is not the case for disabledlib.php
// definitions as they use load_adhoc(). They are built as a new object on each call.
// We do not need to clone the definition because we know it's new.
$definition = $this->create_definition($component, $area);
$definition->set_identifiers($identifiers);
$cache = $this->create_cache($definition);
return $cache;
}
/**
* Removes all temporary caches.
*
* Don't call this directly - used by {@see \core_cache\allow_temporary_caches}.
*/
public static function clear_temporary_caches(): void {
self::$tempcaches = [];
}
/**
* Creates an ad-hoc cache from the given param.
*
* @param int $mode
* @param string $component
* @param string $area
* @param array $identifiers
* @param array $options An array of options, available options are:
* - simplekeys : Set to true if the keys you will use are a-zA-Z0-9_
* - simpledata : Set to true if the type of the data you are going to store is scalar, or an array of scalar vars
* - staticacceleration : If set to true the cache will hold onto all data passing through it.
* - staticaccelerationsize : Sets the max size of the static acceleration array.
* @return cache_application|cache_session|cache_request
*/
public function create_cache_from_params($mode, $component, $area, array $identifiers = array(), array $options = array()) {
// Regular cache definitions are cached inside create_definition(). This is not the case for disabledlib.php
// definitions as they use load_adhoc(). They are built as a new object on each call.
// We do not need to clone the definition because we know it's new.
$definition = cache_definition::load_adhoc($mode, $component, $area, $options);
$definition->set_identifiers($identifiers);
$cache = $this->create_cache($definition);
return $cache;
}
/**
* Creates a store instance given its name and configuration.
*
* @param string $name Unused.
* @param array $details Unused.
* @param cache_definition $definition
* @return boolean|cache_store
*/
public function create_store_from_config($name, array $details, cache_definition $definition) {
return $this->create_dummy_store($definition);
}
/**
* Creates a cache config instance with the ability to write if required.
*
* @param bool $writer Unused.
* @return cache_config_disabled|cache_config_writer
*/
public function create_config_instance($writer = false) {
// We are always going to use the cache_config_disabled class for all regular request.
// However if the code has requested the writer then likely something is changing and
// we're going to need to interact with the config.php file.
// In this case we will still use the cache_config_writer.
$class = 'cache_config_disabled';
if ($writer) {
// If the writer was requested then something is changing.
$class = 'cache_config_writer';
}
if (!array_key_exists($class, $this->configs)) {
self::set_state(self::STATE_INITIALISING);
if ($class === 'cache_config_disabled') {
$configuration = $class::create_default_configuration();
$this->configs[$class] = new $class;
} else {
$configuration = false;
// If we need a writer, we should get the classname from the generic factory.
// This is so alternative classes can be used if a different writer is required.
$this->configs[$class] = parent::get_disabled_writer();
}
$this->configs[$class]->load($configuration);
}
self::set_state(self::STATE_READY);
// Return the instance.
return $this->configs[$class];
}
/**
* Returns true if the cache API has been disabled.
*
* @return bool
*/
public function is_disabled() {
return true;
}
}
/**
* The cache config class used when the Cache has been disabled.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_config_disabled extends cache_config_writer {
/**
* Returns an instance of the configuration writer.
*
* @return cache_config_disabled
*/
public static function instance() {
$factory = cache_factory::instance();
return $factory->create_config_instance(true);
}
/**
* Saves the current configuration.
*/
protected function config_save() {
// Nothing to do here.
}
/**
* Generates a configuration array suitable to be written to the config file.
*
* @return array
*/
protected function generate_configuration_array() {
$configuration = array();
$configuration['stores'] = $this->configstores;
$configuration['modemappings'] = $this->configmodemappings;
$configuration['definitions'] = $this->configdefinitions;
$configuration['definitionmappings'] = $this->configdefinitionmappings;
$configuration['locks'] = $this->configlocks;
return $configuration;
}
/**
* Adds a plugin instance.
*
* @param string $name Unused.
* @param string $plugin Unused.
* @param array $configuration Unused.
* @return bool
* @throws cache_exception
*/
public function add_store_instance($name, $plugin, array $configuration = array()) {
return false;
}
/**
* Sets the mode mappings.
*
* @param array $modemappings Unused.
* @return bool
* @throws cache_exception
*/
public function set_mode_mappings(array $modemappings) {
return false;
}
/**
* Edits a give plugin instance.
*
* @param string $name Unused.
* @param string $plugin Unused.
* @param array $configuration Unused.
* @return bool
* @throws cache_exception
*/
public function edit_store_instance($name, $plugin, $configuration) {
return false;
}
/**
* Deletes a store instance.
*
* @param string $name Unused.
* @return bool
* @throws cache_exception
*/
public function delete_store_instance($name) {
return false;
}
/**
* Creates the default configuration and saves it.
*
* @param bool $forcesave Ignored because we are disabled!
* @return array
*/
public static function create_default_configuration($forcesave = false) {
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_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->configdefinitions = array();
$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->configlocks = array(
'default_file_lock' => array(
'name' => 'cachelock_file_default',
'type' => 'cachelock_file',
'dir' => 'filelocks',
'default' => true
)
);
return $writer->generate_configuration_array();
}
/**
* Updates the definition in the configuration from those found in the cache files.
*
* @param bool $coreonly Unused.
*/
public static function update_definitions($coreonly = false) {
// Nothing to do here.
}
/**
* Locates all of the definition files.
*
* @param bool $coreonly Unused.
* @return array
*/
protected static function locate_definitions($coreonly = false) {
return array();
}
/**
* Sets the mappings for a given definition.
*
* @param string $definition Unused.
* @param array $mappings Unused.
* @throws coding_exception
*/
public function set_definition_mappings($definition, $mappings) {
// Nothing to do here.
}
}
+385
View File
@@ -0,0 +1,385 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 cachestore_addinstance_form extends moodleform {
/**
* The definition of the add instance form
*/
final protected function definition() {
$form = $this->_form;
$store = $this->_customdata['store'];
$plugin = $this->_customdata['plugin'];
$locks = $this->_customdata['locks'];
$form->addElement('hidden', 'plugin', $plugin);
$form->setType('plugin', PARAM_PLUGIN);
$form->addElement('hidden', 'editing', !empty($this->_customdata['store']));
$form->setType('editing', PARAM_BOOL);
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_NOTAGS);
} else {
$form->addElement('hidden', 'name', $store);
$form->addElement('static', 'name-value', get_string('storename', 'cache'), $store);
$form->setType('name', PARAM_NOTAGS);
}
if (is_array($locks)) {
$form->addElement('select', 'lock', get_string('locking', 'cache'), $locks);
$form->addHelpButton('lock', 'locking', 'cache');
$form->setType('lock', PARAM_ALPHANUMEXT);
} else {
$form->addElement('hidden', 'lock', '');
$form->setType('lock', PARAM_ALPHANUMEXT);
$form->addElement('static', 'lock-value', get_string('locking', 'cache'),
'<em>'.get_string('nativelocking', 'cache').'</em>');
}
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 = core_cache\administration_helper::get_store_instance_summaries();
if (array_key_exists($data['name'], $stores)) {
$errors['name'] = get_string('storenamealreadyused', 'cache');
}
}
}
if (method_exists($this, 'configuration_validation')) {
$newerrors = $this->configuration_validation($data, $files, $errors);
// We need to selectiviliy merge here
foreach ($newerrors as $element => $error) {
if (!array_key_exists($element, $errors)) {
$errors[$element] = $error;
}
}
}
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
*/
final protected function definition() {
global $OUTPUT;
$definition = $this->_customdata['definition'];
$form = $this->_form;
list($component, $area) = explode('/', $definition, 2);
list($currentstores, $storeoptions, $defaults) =
core_cache\administration_helper::get_definition_store_options($component, $area);
$storedata = core_cache\administration_helper::get_definition_summaries();
if ($storedata[$definition]['mode'] != cache_store::MODE_REQUEST) {
if (isset($storedata[$definition]['canuselocalstore']) && $storedata[$definition]['canuselocalstore']) {
$form->addElement('html', $OUTPUT->notification(get_string('localstorenotification', 'cache'), 'notifymessage'));
} else {
$form->addElement('html', $OUTPUT->notification(get_string('sharedstorenotification', 'cache'), 'notifymessage'));
}
}
$form->addElement('hidden', 'definition', $definition);
$form->setType('definition', PARAM_SAFEPATH);
$form->addElement('hidden', 'action', 'editdefinitionmapping');
$form->setType('action', PARAM_ALPHA);
$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 definition sharing option
*
* @package core
* @category cache
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_definition_sharing_form extends moodleform {
/**
* The definition of the form
*/
final protected function definition() {
$definition = $this->_customdata['definition'];
$sharingoptions = $this->_customdata['sharingoptions'];
$form = $this->_form;
$form->addElement('hidden', 'definition', $definition);
$form->setType('definition', PARAM_SAFEPATH);
$form->addElement('hidden', 'action', 'editdefinitionsharing');
$form->setType('action', PARAM_ALPHA);
// We use a group here for validation.
$count = 0;
$group = array();
foreach ($sharingoptions as $value => $text) {
$count++;
$group[] = $form->createElement('checkbox', $value, null, $text);
}
$form->addGroup($group, 'sharing', get_string('sharing', 'cache'), '<br />');
$form->setType('sharing', PARAM_INT);
$form->addElement('text', 'userinputsharingkey', get_string('userinputsharingkey', 'cache'));
$form->addHelpButton('userinputsharingkey', 'userinputsharingkey', 'cache');
$form->disabledIf('userinputsharingkey', 'sharing['.cache_definition::SHARING_INPUT.']', 'notchecked');
$form->setType('userinputsharingkey', PARAM_ALPHANUMEXT);
$values = array_keys($sharingoptions);
if (in_array(cache_definition::SHARING_ALL, $values)) {
// If you share with all thenthe other options don't really make sense.
foreach ($values as $value) {
$form->disabledIf('sharing['.$value.']', 'sharing['.cache_definition::SHARING_ALL.']', 'checked');
}
$form->disabledIf('userinputsharingkey', 'sharing['.cache_definition::SHARING_ALL.']', 'checked');
}
$this->add_action_buttons();
}
/**
* Sets the data for this form.
*
* @param array $data
*/
public function set_data($data) {
if (!isset($data['sharing'])) {
// Set the default value here. mforms doesn't handle defaults very nicely.
$data['sharing'] = core_cache\administration_helper::get_definition_sharing_options(cache_definition::SHARING_DEFAULT);
}
parent::set_data($data);
}
/**
* Validates this form
*
* @param array $data
* @param array $files
* @return array
*/
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (count($errors) === 0 && !isset($data['sharing'])) {
// They must select at least one sharing option.
$errors['sharing'] = get_string('sharingrequired', 'cache');
}
return $errors;
}
}
/**
* 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 && ($mode !== cache_store::MODE_SESSION || $store['supports']['searchable'])) {
if (empty($store['default'])) {
$options[$mode][$storename] = $store['name'];
} else {
$options[$mode][$storename] = get_string('store_'.$store['name'], 'cache');
}
}
}
}
$form->addElement('hidden', 'action', 'editmodemappings');
$form->setType('action', PARAM_ALPHA);
foreach ($options as $mode => $optionset) {
$form->addElement('select', 'mode_'.$mode, get_string('mode_'.$mode, 'cache'), $optionset);
}
$this->add_action_buttons();
}
}
/**
* Form to add a cache lock instance.
*
* All cache lock plugins that wish to have custom configuration should override
* this form, and more explicitly the plugin_definition and plugin_validation methods.
*
* @package core
* @category cache
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_lock_form extends moodleform {
/**
* Defines this form.
*/
final public function definition() {
$plugin = $this->_customdata['lock'];
$this->_form->addElement('hidden', 'action', 'newlockinstance');
$this->_form->setType('action', PARAM_ALPHANUMEXT);
$this->_form->addElement('hidden', 'lock', $plugin);
$this->_form->setType('lock', PARAM_COMPONENT);
$this->_form->addElement('text', 'name', get_string('lockname', 'cache'));
$this->_form->setType('name', PARAM_ALPHANUMEXT);
$this->_form->addRule('name', get_string('required'), 'required');
$this->_form->addElement('static', 'namedesc', '', get_string('locknamedesc', 'cache'));
$this->plugin_definition();
$this->add_action_buttons();
}
/**
* Validates this form.
*
* @param array $data
* @param array $files
* @return array
*/
final public function validation($data, $files) {
$errors = parent::validation($data, $files);
if (!isset($errors['name'])) {
$config = cache_config::instance();
if (in_array($data['name'], array_keys($config->get_locks()))) {
$errors['name'] = get_string('locknamenotunique', 'cache');
}
}
$errors = $this->plugin_validation($data, $files, $errors);
return $errors;
}
/**
* Plugin specific definition.
*/
public function plugin_definition() {
// No custom validation going on here.
}
/**
* Plugin specific validation.
*
* @param array $data
* @param array $files
* @param array $errors
* @return array
*/
public function plugin_validation($data, $files, array $errors) {
return $errors;
}
}
+228
View File
@@ -0,0 +1,228 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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/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/store.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 {
/**
* Constructs a new exception
*
* @param string $errorcode
* @param string $module
* @param string $link
* @param mixed $a
* @param mixed $debuginfo
*/
public function __construct($errorcode, $module = 'cache', $link = '', $a = null, $debuginfo = null) {
// This may appear like a useless override but you will notice that we have set a MUCH more useful default for $module.
parent::__construct($errorcode, $module, $link, $a, $debuginfo);
}
}
/**
* An array of cacheable objects.
*
* This class allows a developer to create an array of cacheable objects and store that.
* The cache API doesn't check items within an array to see whether they are cacheable. Such a check would be very costly to both
* arrays using cacheable object and those that don't.
* Instead the developer must explicitly use a cacheable_object_array instance.
*
* The following is one example of how this class can be used.
* <code>
* $data = array();
* $data[] = new cacheable_object('one');
* $data[] = new cacheable_object('two');
* $data[] = new cacheable_object('three');
* $cache->set(new cacheable_object_array($data));
* </code>
* Another example would be
* <code>
* $data = new cacheable_object_array();
* $data[] = new cacheable_object('one');
* $data[] = new cacheable_object('two');
* $data[] = new cacheable_object('three');
* $cache->set($data);
* </code>
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cacheable_object_array extends ArrayObject implements cacheable_object {
/**
* Constructs a new array object instance.
* @param array $items
*/
final public function __construct(array $items = array()) {
parent::__construct($items, ArrayObject::STD_PROP_LIST);
}
/**
* Returns the data to cache for this object.
*
* @return array An array of cache_cached_object instances.
* @throws coding_exception
*/
final public function prepare_to_cache() {
$result = array();
foreach ($this as $key => $value) {
if ($value instanceof cacheable_object) {
$value = new cache_cached_object($value);
} else {
throw new coding_exception('Only cacheable_object instances can be added to a cacheable_array');
}
$result[$key] = $value;
}
return $result;
}
/**
* Returns the cacheable_object_array that was originally sent to the cache.
*
* @param array $data
* @return cacheable_object_array
* @throws coding_exception
*/
final public static function wake_from_cache($data) {
if (!is_array($data)) {
throw new coding_exception('Invalid data type when reviving cacheable_array data');
}
$result = array();
foreach ($data as $key => $value) {
$result[$key] = $value->restore_object();
}
$class = __CLASS__;
return new $class($result);
}
}
+674
View File
@@ -0,0 +1,674 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 {
/**
* Switch that gets set to true when ever a cache_config_writer instance is saving the cache configuration file.
* If this is set to true when save is next called we must avoid the trying to save and instead return the
* generated config so that is may be used instead of the file.
* @var bool
*/
protected static $creatingconfig = false;
/**
* 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.
*
* Exceptions within this function are tolerated but must be of type cache_exception.
* They are caught during initialisation and written to the error log. This is required in order to avoid
* infinite loop situations caused by the cache throwing exceptions during its initialisation.
*/
protected function config_save() {
global $CFG;
static $confighash = '';
$cachefile = static::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. Check the permissions on your moodledata directory.');
}
}
if (!file_exists($directory) || !is_writable($directory)) {
throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Config directory is not writable. Check the permissions on the moodledata/muc directory.');
}
// Prepare a configuration array to store.
$configuration = $this->generate_configuration_array();
// Prepare the file content.
$content = "<?php defined('MOODLE_INTERNAL') || die();\n \$configuration = ".var_export($configuration, true).";";
// Do both file content and hash based detection because this might be called
// many times within a single request.
$hash = sha1($content);
if (($hash === $confighash) || (file_exists($cachefile) && $content === file_get_contents($cachefile))) {
// Config is unchanged so don't bother locking and writing.
$confighash = $hash;
return;
}
// We need to create a temporary cache lock instance for use here. Remember we are generating the config file
// it doesn't exist and thus we can't use the normal API for this (it'll just try to use config).
$lockconf = reset($this->configlocks);
if ($lockconf === false) {
debugging('Your cache configuration file is out of date and needs to be refreshed.', DEBUG_DEVELOPER);
// Use the default
$lockconf = array(
'name' => 'cachelock_file_default',
'type' => 'cachelock_file',
'dir' => 'filelocks',
'default' => true
);
}
$factory = cache_factory::instance();
$locking = $factory->create_lock_instance($lockconf);
if ($locking->lock('configwrite', 'config', true)) {
$tempcachefile = "{$cachefile}.tmp";
// Its safe to use w mode here because we have already acquired the lock.
$handle = fopen($tempcachefile, 'w');
fwrite($handle, $content);
fflush($handle);
fclose($handle);
$locking->unlock('configwrite', 'config');
@chmod($tempcachefile, $CFG->filepermissions);
rename($tempcachefile, $cachefile);
// Tell PHP to recompile the script.
core_component::invalidate_opcode_php_cache($cachefile);
} else {
throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Unable to open the cache config file.');
}
}
/**
* Generates a configuration array suitable to be written to the config file.
* @return array
*/
protected function generate_configuration_array() {
$configuration = array();
$configuration['siteidentifier'] = $this->siteidentifier;
$configuration['stores'] = $this->configstores;
$configuration['modemappings'] = $this->configmodemappings;
$configuration['definitions'] = $this->configdefinitions;
$configuration['definitionmappings'] = $this->configdefinitionmappings;
$configuration['locks'] = $this->configlocks;
return $configuration;
}
/**
* Adds a plugin 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 for the instance (must be unique)
* @param string $plugin The name of the plugin.
* @param array $configuration The configuration data for the plugin instance.
* @return bool
* @throws cache_exception
*/
public function add_store_instance($name, $plugin, array $configuration = array()) {
if (array_key_exists($name, $this->configstores)) {
throw new cache_exception('Duplicate name specificed for cache plugin instance. You must provide a unique name.');
}
$class = 'cachestore_'.$plugin;
if (!class_exists($class)) {
$plugins = core_component::get_plugin_list_with_file('cachestore', 'lib.php');
if (!array_key_exists($plugin, $plugins)) {
throw new cache_exception('Invalid plugin name specified. The plugin does not exist or is not valid.');
}
$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.');
}
}
$reflection = new ReflectionClass($class);
if (!$reflection->isSubclassOf('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']),
'class' => $class,
'default' => false
);
if (array_key_exists('lock', $configuration)) {
$this->configstores[$name]['lock'] = $configuration['lock'];
unset($this->configstores[$name]['configuration']['lock']);
}
// Call instance_created()
$store = new $class($name, $this->configstores[$name]['configuration']);
$store->instance_created();
$this->config_save();
return true;
}
/**
* Adds a new lock instance to the config file.
*
* @param string $name The name the user gave the instance. PARAM_ALHPANUMEXT
* @param string $plugin The plugin we are creating an instance of.
* @param string $configuration Configuration data from the config instance.
* @throws cache_exception
*/
public function add_lock_instance($name, $plugin, $configuration = array()) {
if (array_key_exists($name, $this->configlocks)) {
throw new cache_exception('Duplicate name specificed for cache lock instance. You must provide a unique name.');
}
$class = 'cachelock_'.$plugin;
if (!class_exists($class)) {
$plugins = core_component::get_plugin_list_with_file('cachelock', 'lib.php');
if (!array_key_exists($plugin, $plugins)) {
throw new cache_exception('Invalid lock name specified. The plugin does not exist or is not valid.');
}
$file = $plugins[$plugin];
if (file_exists($file)) {
require_once($file);
}
if (!class_exists($class)) {
throw new cache_exception('Invalid lock plugin specified. The plugin does not contain the required class.');
}
}
$reflection = new ReflectionClass($class);
if (!$reflection->implementsInterface('cache_lock_interface')) {
throw new cache_exception('Invalid lock plugin specified. The plugin does not implement the required interface.');
}
$this->configlocks[$name] = array_merge($configuration, array(
'name' => $name,
'type' => 'cachelock_'.$plugin,
'default' => false
));
$this->config_save();
}
/**
* Deletes a lock instance given its name.
*
* @param string $name The name of the plugin, PARAM_ALPHANUMEXT.
* @return bool
* @throws cache_exception
*/
public function delete_lock_instance($name) {
if (!array_key_exists($name, $this->configlocks)) {
throw new cache_exception('The requested store does not exist.');
}
if ($this->configlocks[$name]['default']) {
throw new cache_exception('You can not delete the default lock.');
}
foreach ($this->configstores as $store) {
if (isset($store['lock']) && $store['lock'] === $name) {
throw new cache_exception('You cannot delete a cache lock that is being used by a store.');
}
}
unset($this->configlocks[$name]);
$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 is 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_store_instance($name, $plugin, $configuration) {
if (!array_key_exists($name, $this->configstores)) {
throw new cache_exception('The requested instance does not exist.');
}
$plugins = core_component::get_plugin_list_with_file('cachestore', '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 = 'cachestore_'.$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 required class.'.$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']),
'class' => $class,
'default' => $this->configstores[$name]['default'] // Can't change the default.
);
if (array_key_exists('lock', $configuration)) {
$this->configstores[$name]['lock'] = $configuration['lock'];
unset($this->configstores[$name]['configuration']['lock']);
}
$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_instance($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.');
}
}
// Call instance_deleted()
$class = 'cachestore_'.$this->configstores[$name]['plugin'];
$store = new $class($name, $this->configstores[$name]['configuration']);
$store->instance_deleted();
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.
*
* @param bool $forcesave If set to true then we will forcefully save the default configuration file.
* @return true|array Returns true if the default configuration was successfully created.
* Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs.
*/
public static function create_default_configuration($forcesave = false) {
// 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.
$writer = new self;
$writer->configstores = self::get_default_stores();
$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->configlocks = array(
'default_file_lock' => array(
'name' => 'cachelock_file_default',
'type' => 'cachelock_file',
'dir' => 'filelocks',
'default' => true
)
);
$factory = cache_factory::instance();
// We expect the cache to be initialising presently. If its not then something has gone wrong and likely
// we are now in a loop.
if (!$forcesave && $factory->get_state() !== cache_factory::STATE_INITIALISING) {
return $writer->generate_configuration_array();
}
$factory->set_state(cache_factory::STATE_SAVING);
$writer->config_save();
return true;
}
/**
* Returns an array of default stores for use.
*
* @return array
*/
protected static function get_default_stores() {
global $CFG;
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');
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.
*
* Calls config_save further down, you should redirect immediately or asap after calling this method.
*
* @param bool $coreonly If set to true only core definitions will be updated.
*/
public static function update_definitions($coreonly = false) {
$factory = cache_factory::instance();
$factory->updating_started();
$config = $factory->create_config_instance(true);
$config->write_definitions_to_cache(self::locate_definitions($coreonly));
$factory->updating_finished();
}
/**
* Locates all of the definition files.
*
* @param bool $coreonly If set to true only core definitions will be updated.
* @return array
*/
protected static function locate_definitions($coreonly = false) {
global $CFG;
$files = array();
if (file_exists($CFG->dirroot.'/lib/db/caches.php')) {
$files['core'] = $CFG->dirroot.'/lib/db/caches.php';
}
if (!$coreonly) {
$plugintypes = core_component::get_plugin_types();
foreach ($plugintypes as $type => $location) {
$plugins = core_component::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 id: '.$id, 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) {
// Preserve the selected sharing option when updating the definitions.
// This is set by the user and should never come from caches.php.
foreach ($definitions as $key => $definition) {
unset($definitions[$key]['selectedsharingoption']);
unset($definitions[$key]['userinputsharingkey']);
if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['selectedsharingoption'])) {
$definitions[$key]['selectedsharingoption'] = $this->configdefinitions[$key]['selectedsharingoption'];
}
if (isset($this->configdefinitions[$key]) && isset($this->configdefinitions[$key]['userinputsharingkey'])) {
$definitions[$key]['userinputsharingkey'] = $this->configdefinitions[$key]['userinputsharingkey'];
}
}
$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();
}
/**
* Update the site identifier stored by the cache API.
*
* @param string $siteidentifier
* @return string The new site identifier.
*/
public function update_site_identifier($siteidentifier) {
$this->siteidentifier = md5((string)$siteidentifier);
$this->config_save();
return $this->siteidentifier;
}
/**
* Sets the selected sharing options and key for a definition.
*
* @param string $definition The name of the definition to set for.
* @param int $sharingoption The sharing option to set.
* @param string|null $userinputsharingkey The user input key or null.
* @throws coding_exception
*/
public function set_definition_sharing($definition, $sharingoption, $userinputsharingkey = null) {
if (!array_key_exists($definition, $this->configdefinitions)) {
throw new coding_exception('Invalid definition name passed when updating sharing options.');
}
if (!($this->configdefinitions[$definition]['sharingoptions'] & $sharingoption)) {
throw new coding_exception('Invalid sharing option passed when updating definition.');
}
$this->configdefinitions[$definition]['selectedsharingoption'] = (int)$sharingoption;
if (!empty($userinputsharingkey)) {
$this->configdefinitions[$definition]['userinputsharingkey'] = (string)$userinputsharingkey;
}
$this->config_save();
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachelock_file.
*
* @package cachelock_file
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachelock_file\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachelock_file implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for the cache file locking plugin
*
* @package cachelock_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'File locking';
$string['privacy:metadata'] = 'The File locking plugin does not store any personal data.';
+237
View File
@@ -0,0 +1,237 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File locking for the Cache API
*
* @package cachelock_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* File locking plugin
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachelock_file implements cache_lock_interface {
/**
* The name of the cache lock instance
* @var string
*/
protected $name;
/**
* The absolute directory in which lock files will be created and looked for.
* @var string
*/
protected $cachedir;
/**
* The maximum life in seconds for a lock file. By default null for none.
* @var int|null
*/
protected $maxlife = null;
/**
* The number of attempts to acquire a lock when blocking is required before throwing an exception.
* @var int
*/
protected $blockattempts = 100;
/**
* An array containing the locks that have been acquired but not released so far.
* @var array Array of key => lock file path
*/
protected $locks = array();
/**
* Initialises the cache lock instance.
*
* @param string $name The name of the cache lock
* @param array $configuration
*/
public function __construct($name, array $configuration = array()) {
$this->name = $name;
if (!array_key_exists('dir', $configuration)) {
$this->cachedir = make_cache_directory(md5($name));
} else {
$dir = $configuration['dir'];
if (strpos($dir, '/') !== false && strpos($dir, '.') !== 0) {
// This looks like an absolute path.
if (file_exists($dir) && is_dir($dir) && is_writable($dir)) {
$this->cachedir = $dir;
}
}
if (empty($this->cachedir)) {
$dir = preg_replace('#[^a-zA-Z0-9_]#', '_', $dir);
$this->cachedir = make_cache_directory($dir);
}
}
if (array_key_exists('maxlife', $configuration) && is_number($configuration['maxlife'])) {
$maxlife = (int)$configuration['maxlife'];
// Minimum lock time is 60 seconds.
$this->maxlife = max($maxlife, 60);
}
if (array_key_exists('blockattempts', $configuration) && is_number($configuration['blockattempts'])) {
$this->blockattempts = (int)$configuration['blockattempts'];
}
}
/**
* 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 string $ownerid A unique identifier for the owner of this lock. Not used by default.
* @param bool $block True if we want the program block further execution until the lock has been acquired.
* @return bool
* @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock.
*/
public function lock($key, $ownerid, $block = false) {
// 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 ($this->maxlife !== null && !array_key_exists($key, $this->locks)) {
$mtime = filemtime($lockfile);
if ($mtime < time() - $this->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 = $this->blockattempts;
while (($result = $this->lock($key, false)) === false) {
// Usleep causes the application to cleep to x microseconds.
// Before anyone asks there are 1'000'000 microseconds to a second.
usleep(rand(1000, 50000)); // Sleep between 1 and 50 milliseconds.
$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 {@link cache_lock::unlock()}
*
* @param string $key
* @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
* @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
* @return bool
*/
public function unlock($key, $ownerid, $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;
}
/**
* Checks if the given key is locked.
*
* @param string $key
* @param string $ownerid
*/
public function check_state($key, $ownerid) {
if (array_key_exists($key, $this->locks)) {
// The key is locked and we own it.
return true;
}
$lockfile = $this->get_lock_file($key);
if (file_exists($lockfile)) {
// The key is locked and we don't own it.
return false;
}
return null;
}
/**
* Gets the name to use for a lock file.
*
* @param string $key
* @return string
*/
protected function get_lock_file($key) {
return $this->cachedir.'/'. $key .'.lock';
}
/**
* Cleans up the instance what it is no longer needed.
*/
public function __destruct() {
foreach ($this->locks as $lockfile) {
// Naught, naughty developers.
@unlink($lockfile);
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* File locking for the Cache API
*
* @package cachelock_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'cachelock_file'; // Full name of the plugin (used for diagnostics)
+85
View File
@@ -0,0 +1,85 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the apcu cache store.
*
* This file is part of the apcu cache store, it contains the API for interacting with an instance of the store.
*
* @package cachestore_apcu
* @copyright 2014 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/cache/forms.php');
/**
* Form for adding a apcu instance.
*
* @copyright 2014 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_apcu_addinstance_form extends cachestore_addinstance_form {
/**
* Add the desired form elements.
*/
protected function configuration_definition() {
global $CFG;
$form = $this->_form;
$form->addElement('text', 'prefix', get_string('prefix', 'cachestore_apcu'),
array('maxlength' => 5, 'size' => 5));
$form->addHelpButton('prefix', 'prefix', 'cachestore_apcu');
$form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
$form->setDefault('prefix', $CFG->prefix);
$form->addRule('prefix', get_string('prefixinvalid', 'cachestore_apcu'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
$form->addElement('header', 'apc_notice', get_string('notice', 'cachestore_apcu'));
$form->setExpanded('apc_notice');
$link = get_docs_url('Caching#APC');
$form->addElement('html', nl2br(get_string('clusternotice', 'cachestore_apcu', $link)));
}
/**
* Validates the configuration data.
*
* We need to check that prefix is unique.
*
* @param array $data
* @param array $files
* @param array $errors
* @return array
* @throws coding_exception
*/
public function configuration_validation($data, $files, array $errors) {
if (empty($errors['prefix'])) {
$factory = cache_factory::instance();
$config = $factory->create_config_instance();
foreach ($config->get_all_stores() as $store) {
if ($store['plugin'] === 'apcu') {
if (isset($store['configuration']['prefix'])) {
if ($data['prefix'] === $store['configuration']['prefix']) {
// The new store has the same prefix as an existing store, thats a problem.
$errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu');
break;
}
} else if (empty($data['prefix'])) {
// The existing store hasn't got a prefix and neither does the new store, that's a problem.
$errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu');
break;
}
}
}
}
return $errors;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachestore_apcu.
*
* @package cachestore_apcu
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachestore_apcu\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachestore_apcu implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* APCu cache store language strings.
*
* @package cachestore_apcu
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['clusternotice'] = 'Please be aware that APCu is only a suitable choice for single node sites or caches that can be stored locally.
For more information, see the <a href="{$a}">APC user cache documentation</a>.';
$string['notice'] = 'Notice';
$string['pluginname'] = 'APC user cache (APCu)';
$string['prefix'] = 'Prefix';
$string['prefix_help'] = 'The above prefix gets used for all keys being stored in this APC store instance. By default the database prefix is used.';
$string['prefixinvalid'] = 'The prefix you have selected is invalid. You can only use a-z A-Z 0-9-_.';
$string['prefixnotunique'] = 'The prefix you have selected is not unique. Please choose a unique prefix.';
$string['privacy:metadata'] = 'The APC user cache (APCu) plugin stores data briefly as part of its caching functionality but this data is regularly cleared and is not sent externally in any way.';
$string['testperformance'] = 'Test performance';
$string['testperformance_desc'] = 'If enabled, APCu performance will be included when viewing the Test performance page. Enabling this on a production site is not recommended.';
+412
View File
@@ -0,0 +1,412 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* APCu cache store main library.
*
* @package cachestore_apcu
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The APCu cache store class.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_is_configurable {
/**
* The required version of APCu for this extension.
*/
const REQUIRED_VERSION = '4.0.0';
/**
* The name of this store instance.
* @var string
*/
protected $name;
/**
* The definition used when this instance was initialised.
* @var cache_definition
*/
protected $definition = null;
/**
* The storeprefix to use on all instances of this store. Configured as part store setup.
* @var string
*/
protected $storeprefix = null;
/**
* The prefix added specifically for this cache.
* @var string
*/
protected $cacheprefix = null;
/**
* Static method to check that the APCu stores requirements have been met.
*
* It checks that the APCu extension has been loaded and that it has been enabled.
*
* @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() {
$enabled = ini_get('apc.enabled') && (php_sapi_name() != "cli" || ini_get('apc.enable_cli'));
if (!extension_loaded('apcu') || !$enabled) {
return false;
}
$version = phpversion('apcu');
return $version && version_compare($version, self::REQUIRED_VERSION, '>=');
}
/**
* Static method to check if a store is usable with the given mode.
*
* @param int $mode One of cache_store::MODE_*
* @return bool True if the mode is supported.
*/
public static function is_supported_mode($mode) {
return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
}
/**
* 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()) {
return self::SUPPORTS_NATIVE_TTL;
}
/**
* 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()) {
return self::MODE_APPLICATION + self::MODE_SESSION;
}
/**
* 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()) {
global $CFG;
$this->name = $name;
$this->storeprefix = $CFG->prefix;
if (isset($configuration['prefix'])) {
$this->storeprefix = $configuration['prefix'];
}
}
/**
* Returns the name of this store instance.
* @return string
*/
public function my_name() {
return $this->name;
}
/**
* 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
* @return bool
*/
public function initialise(cache_definition $definition) {
$this->definition = $definition;
$this->cacheprefix = $this->storeprefix.$definition->generate_definition_hash().'__';
return true;
}
/**
* Returns true if this cache store instance has been initialised.
* @return bool
*/
public function is_initialised() {
return ($this->definition !== null);
}
/**
* Prepares the given key for use.
*
* Should be called before all interaction.
*
* @param string $key The key to prepare for storing in APCu.
*
* @return string
*/
protected function prepare_key($key) {
return $this->cacheprefix . $key;
}
/**
* 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) {
$key = $this->prepare_key($key);
$success = false;
$outcome = apcu_fetch($key, $success);
if ($success) {
return $outcome;
}
return $success;
}
/**
* 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) {
$map = array();
foreach ($keys as $key) {
$map[$key] = $this->prepare_key($key);
}
$outcomes = array();
$success = false;
$results = apcu_fetch($map, $success);
if ($success) {
foreach ($map as $key => $used) {
if (array_key_exists($used, $results)) {
$outcomes[$key] = $results[$used];
} else {
$outcomes[$key] = false;
}
}
} else {
$outcomes = array_fill_keys($keys, false);
}
return $outcomes;
}
/**
* 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) {
$key = $this->prepare_key($key);
return apcu_store($key, $data, $this->definition->get_ttl());
}
/**
* 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) {
$map = array();
foreach ($keyvaluearray as $pair) {
$key = $this->prepare_key($pair['key']);
$map[$key] = $pair['value'];
}
$result = apcu_store($map, null, $this->definition->get_ttl());
return count($map) - count($result);
}
/**
* 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) {
$key = $this->prepare_key($key);
return apcu_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) {
$count = 0;
foreach ($keys as $key) {
if ($this->delete($key)) {
$count++;
}
}
return $count;
}
/**
* Purges the cache deleting all items within it.
*
* @return boolean True on success. False otherwise.
*/
public function purge() {
if (class_exists('APCUIterator', false)) {
$iterator = new APCUIterator('#^' . preg_quote($this->cacheprefix, '#') . '#');
} else {
$iterator = new APCIterator('user', '#^' . preg_quote($this->cacheprefix, '#') . '#');
}
return apcu_delete($iterator);
}
/**
* Performs any necessary clean up when the store instance is being deleted.
*/
public function instance_deleted() {
if (class_exists('APCUIterator', false)) {
$iterator = new APCUIterator('#^' . preg_quote($this->storeprefix, '#') . '#');
} else {
$iterator = new APCIterator('user', '#^' . preg_quote($this->storeprefix, '#') . '#');
}
return apcu_delete($iterator);
}
/**
* 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
*/
public static function initialise_test_instance(cache_definition $definition) {
$testperformance = get_config('cachestore_apcu', 'testperformance');
if (empty($testperformance)) {
return false;
}
if (!self::are_requirements_met()) {
return false;
}
$name = 'APCu test';
$cache = new cachestore_apcu($name);
// No need to check if is_ready() as this has already being done by requirement check.
$cache->initialise($definition);
return $cache;
}
/**
* Test is a cache has a key.
*
* @param string|int $key
* @return bool True if the cache has the requested key, false otherwise.
*/
public function has($key) {
$key = $this->prepare_key($key);
return apcu_exists($key);
}
/**
* Test if a cache has at least one of the given keys.
*
* @param array $keys
* @return bool True if the cache has at least one of the given keys
*/
public function has_any(array $keys) {
foreach ($keys as $arraykey => $key) {
$keys[$arraykey] = $this->prepare_key($key);
}
$result = apcu_exists($keys);
return count($result) > 0;
}
/**
* Test is a cache has all of the given keys.
*
* @param array $keys
* @return bool True if the cache has all of the given keys, false otherwise.
*/
public function has_all(array $keys) {
foreach ($keys as $arraykey => $key) {
$keys[$arraykey] = $this->prepare_key($key);
}
$result = apcu_exists($keys);
return count($result) === count($keys);
}
/**
* Generates the appropriate configuration required for unit testing.
*
* @return array Array of unit test configuration data to be used by initialise().
*/
public static function unit_test_configuration() {
return array('prefix' => 'phpunit');
}
/**
* Given the data from the add instance form this function creates a configuration array.
*
* @param stdClass $data
* @return array
*/
public static function config_get_configuration_array($data) {
$config = array();
if (isset($data->prefix)) {
$config['prefix'] = $data->prefix;
}
return $config;
}
/**
* Allows the cache store to set its data against the edit form before it is shown to the user.
*
* @param moodleform $editform
* @param array $config
*/
public static function config_set_edit_form_data(moodleform $editform, array $config) {
if (isset($config['prefix'])) {
$data['prefix'] = $config['prefix'];
} else {
$data['prefix'] = '';
}
$editform->set_data($data);
}
/**
* Returns true if this cache store instance is both suitable for testing, and ready for testing.
*
* Cache stores that support being used as the default store for unit and acceptance testing should
* override this function and return true if there requirements have been met.
*
* @return bool
*/
public static function ready_to_be_used_for_testing() {
return true;
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The settings for the APCu store.
*
* This file is part of the APCu cache store, it contains the API for interacting with an instance of the store.
*
* @package cachestore_apcu
* @copyright 2014 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$settings->add(
new admin_setting_configcheckbox(
'cachestore_apcu/testperformance',
new lang_string('testperformance', 'cachestore_apcu'),
new lang_string('testperformance_desc', 'cachestore_apcu'),
false
)
);
+89
View File
@@ -0,0 +1,89 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_apcu;
use cache_store;
use cache_definition;
use cachestore_apcu;
defined('MOODLE_INTERNAL') || die();
// Include the necessary evils.
global $CFG;
require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
require_once($CFG->dirroot.'/cache/stores/apcu/lib.php');
/**
* APC unit test class.
*
* @package cachestore_apcu
* @copyright 2014 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \cachestore_tests {
/**
* Returns the apcu class name
* @return string
*/
protected function get_class_name() {
return 'cachestore_apcu';
}
public function setUp(): void {
if (!cachestore_apcu::are_requirements_met()) {
$this->markTestSkipped('Could not test cachestore_apcu. Requirements are not met.');
}
parent::setUp();
}
/**
* Test that the Moodle APCu store doesn't cross paths with other code using APCu as well.
*/
public function test_cross_application_interaction(): void {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test');
$instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
$instance->initialise($definition);
// Test purge with custom data.
$this->assertTrue($instance->set('test', 'monster'));
$this->assertSame('monster', $instance->get('test'));
$this->assertTrue(apcu_store('test', 'pirate', 180));
$this->assertSame('monster', $instance->get('test'));
$this->assertTrue(apcu_exists('test'));
$this->assertSame('pirate', apcu_fetch('test'));
// Purge and check that our data is gone but the the custom data is still there.
$this->assertTrue($instance->purge());
$this->assertFalse($instance->get('test'));
$this->assertTrue(apcu_exists('test'));
$this->assertSame('pirate', apcu_fetch('test'));
}
public function test_different_caches_have_different_prefixes(): void {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test');
$instance = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
$instance->initialise($definition);
$definition2 = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test2');
$instance2 = new cachestore_apcu('Test', cachestore_apcu::unit_test_configuration());
$instance2->initialise($definition2);
$instance->set('test1', 1);
$this->assertFalse($instance2->get('test1'));
$instance2->purge();
$this->assertSame(1, $instance->get('test1'));
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* APCu cache store version information.
*
* @package cachestore_apcu
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
$plugin->component = 'cachestore_apcu';
+71
View File
@@ -0,0 +1,71 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the file cache store.
*
* This file is part of the file cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once($CFG->dirroot.'/cache/forms.php');
/**
* Form for adding a file instance.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_file_addinstance_form extends cachestore_addinstance_form {
/**
* Adds the desired form elements.
*/
protected function configuration_definition() {
$form = $this->_form;
$form->addElement('text', 'path', get_string('path', 'cachestore_file'));
$form->setType('path', PARAM_RAW);
$form->addHelpButton('path', 'path', 'cachestore_file');
$form->addElement('checkbox', 'autocreate', get_string('autocreate', 'cachestore_file'));
$form->setType('autocreate', PARAM_BOOL);
$form->addHelpButton('autocreate', 'autocreate', 'cachestore_file');
$form->disabledIf('autocreate', 'path', 'eq', '');
$form->addElement('checkbox', 'singledirectory', get_string('singledirectory', 'cachestore_file'));
$form->setType('singledirectory', PARAM_BOOL);
$form->addHelpButton('singledirectory', 'singledirectory', 'cachestore_file');
$form->addElement('checkbox', 'prescan', get_string('prescan', 'cachestore_file'));
$form->setType('prescan', PARAM_BOOL);
$form->addHelpButton('prescan', 'prescan', 'cachestore_file');
$form->addElement('checkbox', 'asyncpurge', get_string('asyncpurge', 'cachestore_file'));
$form->setType('asyncpurge', PARAM_BOOL);
$form->addHelpButton('asyncpurge', 'asyncpurge', 'cachestore_file');
$form->addElement('text', 'lockwait', get_string('lockwait', 'cachestore_file'));
$form->setDefault('lockwait', 60);
$form->setType('lockwait', PARAM_INT);
$form->addHelpButton('lockwait', 'lockwait', 'cachestore_file');
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachestore_file.
*
* @package cachestore_file
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachestore_file\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachestore_file implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_file\task;
/**
* Task deletes old cache revision directory.
*
* @package cachestore_file
* @copyright Catalyst IT Europe Ltd 2021
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jackson D'Souza <jackson.dsouza@catalyst-eu.net>
*/
class asyncpurge extends \core\task\adhoc_task {
/**
* Executes the scheduled task.
*
* @return boolean True if old cache revision directory exists and is deleted. False otherwise.
*/
public function execute(): bool {
$returnvar = true;
$output = 'Cleaning up file store old cache revision directory:' . PHP_EOL;
$data = $this->get_custom_data();
if (is_dir($data->path)) {
remove_dir($data->path);
$output .= 'Directory deleted: ' . $data->path;
} else {
$output .= 'Directory not found: ' . $data->path;
$returnvar = false;
}
if (!PHPUNIT_TEST) {
mtrace($output);
}
return $returnvar;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the file cache store.
*
* This file is part of the file cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['asyncpurge'] = 'Asynchronously purge directory';
$string['asyncpurge_help'] = 'If enabled, the new directory is created with cache revision and the old directory will be deleted asynchronously via a scheduled task.';
$string['autocreate'] = 'Auto create directory';
$string['autocreate_help'] = 'If enabled the directory specified in path will be automatically created if it does not already exist.';
$string['lockwait'] = 'Maximum lock wait time';
$string['lockwait_help'] = 'The maximum amount of time in seconds to wait for an exclusive lock before reading or writing a cache key. This is only used for cache definitions that have read or write locking required.';
$string['path'] = 'Cache path';
$string['path_help'] = 'The directory that should be used to store files for this cache store. If left blank (default) a directory will be automatically created in the moodledata directory. This can be used to point a file store towards a directory on a better performing drive (such as one in memory).';
$string['pluginname'] = 'File cache';
$string['privacy:metadata'] = 'The File cache cachestore plugin stores data briefly as part of its caching functionality but this data is regularly cleared.';
$string['prescan'] = 'Prescan directory';
$string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.';
$string['singledirectory'] = 'Single directory store';
$string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories.
Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations.
It is advisable to only turn this on if the following is true:
* If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.
* The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
$string['task_asyncpurge'] = 'Asynchronously purge file store old cache revision directories';
/**
* This is is like the file store, but designed for siutations where:
* - many more things are likely to be stored in the cache, so CRC hashing is
* too likely to give collisions, and storing everything in a completely flat
* directory structure is inadvisable.
* - the things we are caching are more expensive to calculate, so the extra
* time to computer a better hash is a worthwhile trade-off.
*/
+1042
View File
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_file;
use cache_definition;
use cache_store;
use cachestore_file;
/**
* Async purge support test for File cache.
*
* @package cachestore_file
* @copyright Catalyst IT Europe Ltd 2021
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jackson D'Souza <jackson.dsouza@catalyst-eu.net>
* @coversDefaultClass \cachestore_file
*/
class asyncpurge_test extends \advanced_testcase {
/**
* Testing Asynchronous file store cache purge
*
* @covers ::initialise
* @covers ::set
* @covers ::get
* @covers ::purge
*/
public function test_cache_async_purge(): void {
$this->resetAfterTest(true);
// Cache definition.
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_file', 'phpunit_test');
// Extra config, set async purge = true.
$extraconfig = ['asyncpurge' => true, 'filecacherev' => time()];
$configuration = array_merge(cachestore_file::unit_test_configuration(), $extraconfig);
$name = 'File async test';
// Create file cache store.
$cache = new cachestore_file($name, $configuration);
// Initialise file cache store.
$cache->initialise($definition);
$cache->set('foo', 'bar');
$this->assertSame('bar', $cache->get('foo'));
// Purge this file cache store.
$cache->purge();
// Purging file cache store shouldn't purge the data but create a new cache revision directory.
$this->assertSame('bar', $cache->get('foo'));
$cache->set('foo', 'bar 2');
$this->assertSame('bar 2', $cache->get('foo'));
}
/**
* Testing Adhoc Cron - deletes old cache revision directory
*
* @covers \cachestore_file\task
*/
public function test_cache_async_purge_cron(): void {
global $CFG, $USER;
$this->resetAfterTest(true);
$tmpdir = realpath($CFG->tempdir);
$directorypath = '/cachefile_store';
$cacherevdir = $tmpdir . $directorypath;
// Create cache revision directory.
mkdir($cacherevdir, $CFG->directorypermissions, true);
// Create / execute adhoc task to delete cache revision directory.
$asynctask = new cachestore_file\task\asyncpurge();
$asynctask->set_custom_data(['path' => $cacherevdir]);
$asynctask->set_userid($USER->id);
\core\task\manager::queue_adhoc_task($asynctask);
$asynctask->execute();
// Check if cache revision directory has been deleted.
$this->assertDirectoryDoesNotExist($cacherevdir);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_file;
use cache_definition;
use cache_store;
use cachestore_file;
defined('MOODLE_INTERNAL') || die();
// Include the necessary evils.
global $CFG;
require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
require_once($CFG->dirroot.'/cache/stores/file/lib.php');
/**
* File unit test class.
*
* @package cachestore_file
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \cachestore_file
*/
class store_test extends \cachestore_tests {
/**
* Returns the file class name
* @return string
*/
protected function get_class_name() {
return 'cachestore_file';
}
/**
* Testing cachestore_file::get with prescan enabled and with
* deleting the cache between the prescan and the call to get.
*
* The deleting of cache simulates some other process purging
* the cache.
*/
public function test_cache_get_with_prescan_and_purge(): void {
global $CFG;
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test');
$name = 'File test';
$path = make_cache_directory('cachestore_file_test');
$cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
$cache->initialise($definition);
$cache->set('testing', 'value');
$path = make_cache_directory('cachestore_file_test');
$cache = new cachestore_file($name, array('path' => $path, 'prescan' => true));
$cache->initialise($definition);
// Let's pretend that some other process purged caches.
remove_dir($CFG->cachedir.'/cachestore_file_test', true);
make_cache_directory('cachestore_file_test');
$cache->get('testing');
}
/**
* Tests the get_last_read byte count.
*/
public function test_get_last_io_bytes(): void {
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cachestore_file', 'phpunit_test');
$store = new \cachestore_file('Test');
$store->initialise($definition);
$store->set('foo', 'bar');
$store->set('frog', 'ribbit');
$store->get('foo');
// It's not 3 bytes, because the data is stored serialized.
$this->assertEquals(10, $store->get_last_io_bytes());
$store->get('frog');
$this->assertEquals(13, $store->get_last_io_bytes());
$store->get_many(['foo', 'frog']);
$this->assertEquals(23, $store->get_last_io_bytes());
$store->set('foo', 'goo');
$this->assertEquals(10, $store->get_last_io_bytes());
$store->set_many([
['key' => 'foo', 'value' => 'bar'],
['key' => 'frog', 'value' => 'jump']
]);
$this->assertEquals(21, $store->get_last_io_bytes());
}
public function test_lock(): void {
$store = new \cachestore_file('Test');
$this->assertTrue($store->acquire_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '123'));
$this->assertFalse($store->check_lock_state('lock', '321'));
$this->assertNull($store->check_lock_state('notalock', '123'));
$this->assertFalse($store->release_lock('lock', '321'));
$this->assertTrue($store->release_lock('lock', '123'));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache file store version information.
*
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_file
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2024042200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'cachestore_file'; // Full name of the plugin.
+6
View File
@@ -0,0 +1,6 @@
Redis Cache Store for Moodle
============================
A Moodle cache store plugin for [Redis](http://redis.io).
This plugin requires the [PhpRedis](https://github.com/phpredis/phpredis) extension. The PhpRedis extension can be installed via PECL with `pecl install redis`.
+81
View File
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redis Cache Store - Add instance form
*
* @package cachestore_redis
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/cache/forms.php');
/**
* Form for adding instance of Redis Cache Store.
*
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_redis_addinstance_form extends cachestore_addinstance_form {
/**
* Builds the form for creating an instance.
*/
protected function configuration_definition() {
$form = $this->_form;
$form->addElement('advcheckbox', 'clustermode', get_string('clustermode', 'cachestore_redis'), '',
cache_helper::is_cluster_available() ? '' : 'disabled');
$form->addHelpButton('clustermode', 'clustermode', 'cachestore_redis');
$form->setType('clustermode', PARAM_BOOL);
$form->addElement('textarea', 'server', get_string('server', 'cachestore_redis'), ['cols' => 6, 'rows' => 10]);
$form->setType('server', PARAM_TEXT);
$form->addHelpButton('server', 'server', 'cachestore_redis');
$form->addRule('server', get_string('required'), 'required');
$form->addElement('advcheckbox', 'encryption', get_string('encrypt_connection', 'cachestore_redis'));
$form->setType('encryption', PARAM_BOOL);
$form->addHelpButton('encryption', 'encrypt_connection', 'cachestore_redis');
$form->addElement('text', 'cafile', get_string('ca_file', 'cachestore_redis'));
$form->setType('cafile', PARAM_TEXT);
$form->addHelpButton('cafile', 'ca_file', 'cachestore_redis');
$form->addElement('passwordunmask', 'password', get_string('password', 'cachestore_redis'));
$form->setType('password', PARAM_RAW);
$form->addHelpButton('password', 'password', 'cachestore_redis');
$form->addElement('text', 'prefix', get_string('prefix', 'cachestore_redis'), array('size' => 16));
$form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
$form->addHelpButton('prefix', 'prefix', 'cachestore_redis');
$form->addRule('prefix', get_string('prefixinvalid', 'cachestore_redis'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
$serializeroptions = cachestore_redis::config_get_serializer_options();
$form->addElement('select', 'serializer', get_string('useserializer', 'cachestore_redis'), $serializeroptions);
$form->addHelpButton('serializer', 'useserializer', 'cachestore_redis');
$form->setDefault('serializer', Redis::SERIALIZER_PHP);
$form->setType('serializer', PARAM_INT);
$compressoroptions = cachestore_redis::config_get_compressor_options();
$form->addElement('select', 'compressor', get_string('usecompressor', 'cachestore_redis'), $compressoroptions);
$form->addHelpButton('compressor', 'usecompressor', 'cachestore_redis');
$form->setDefault('compressor', cachestore_redis::COMPRESSOR_NONE);
$form->setType('compressor', PARAM_INT);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachestore_redis.
*
* @package cachestore_redis
* @category privacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachestore_redis\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\userlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachestore_redis.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\core_userlist_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_external_location_link('redis', [
'data' => 'privacy:metadata:redis:data',
], 'privacy:metadata:redis');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all use data which matches the specified deletion_criteria.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
}
+105
View File
@@ -0,0 +1,105 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis\task;
/**
* Task deletes old data from Redis caches with TTL set.
*
* @package cachestore_redis
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ttl extends \core\task\scheduled_task {
/** @var int Only display memory savings of at least 100 KB */
const MIN_MEMORY_SIZE = 100 * 1024;
/**
* Gets the name of this task.
*
* @return string Task name
*/
public function get_name(): string {
return get_string('task_ttl', 'cachestore_redis');
}
/**
* Executes the scheduled task.
*/
public function execute(): void {
// Find all Redis cache stores.
$factory = \cache_factory::instance();
$config = $factory->create_config_instance();
$stores = $config->get_all_stores();
$doneanything = false;
foreach ($stores as $storename => $storeconfig) {
if ($storeconfig['plugin'] !== 'redis') {
continue;
}
// For each definition in the cache store, do TTL expiry if needed.
$definitions = $config->get_definitions_by_store($storename);
foreach ($definitions as $definition) {
if (empty($definition['ttl'])) {
continue;
}
if (!empty($definition['requireidentifiers'])) {
// We can't make cache below if it requires identifiers.
continue;
}
$doneanything = true;
$definitionname = $definition['component'] . '/' . $definition['area'];
mtrace($definitionname, ': ');
\cache::make($definition['component'], $definition['area']);
$definition = $factory->create_definition($definition['component'], $definition['area']);
$stores = $factory->get_store_instances_in_use($definition);
foreach ($stores as $store) {
// These were all definitions using a Redis store but one definition may
// potentially have multiple stores, we need to process the Redis ones only.
if (!($store instanceof \cachestore_redis)) {
continue;
}
$info = $store->expire_ttl();
$infotext = 'Deleted ' . $info['keys'] . ' key(s) in ' .
sprintf('%0.2f', $info['time']) . 's';
// Only report memory information if available, positive, and reasonably large.
// Otherwise the real information is hard to see amongst random variation etc.
if (!empty($info['memory']) && $info['memory'] > self::MIN_MEMORY_SIZE) {
$infotext .= ' - reported saving ' . display_size($info['memory']);
}
mtrace($infotext);
}
}
}
if (!$doneanything) {
mtrace('No TTL caches assigned to a Redis store; nothing to do.');
}
}
/**
* Checks if this task is allowed to run - this makes it show the 'Run now' link (or not).
*
* @return bool True if task can run
*/
public function can_run(): bool {
// The default implementation of this function checks the plugin is enabled, which doesn't
// seem to work (probably because cachestore plugins can't be enabled).
// We could check if there is a Redis store configured, but it would have to do the exact
// same logic as already in the first part of 'execute', so it's probably OK to just return
// true.
return true;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Scheduled tasks.
*
* @copyright 2021 The Open University
* @package cachestore_redis
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = [
[
'classname' => '\cachestore_redis\task\ttl',
'blocking' => 0,
'minute' => 'R',
'hour' => '*',
'day' => '*',
'month' => '*',
'dayofweek' => '*',
'disabled' => 0
]
];
+99
View File
@@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redis Cache Store - English language strings
*
* @package cachestore_redis
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['ca_file'] = 'CA file path';
$string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem';
$string['clustermode'] = 'Cluster mode';
$string['clustermode_help'] = 'Enabling cluster mode will run the Redis Cluster function, allowing your server to serve multiple servers to handle concurrent requests simultaneously.';
$string['clustermodeunavailable'] = 'Redis Cluster is currently unavailable. Please ensure that the PHP Redis extension supports Redis Cluster functionality.';
$string['compressor_none'] = 'No compression.';
$string['compressor_php_gzip'] = 'Use gzip compression.';
$string['compressor_php_zstd'] = 'Use Zstandard compression.';
$string['encrypt_connection'] = 'Use TLS encryption.';
$string['encrypt_connection_help'] = 'Use TLS to connect to Redis. Do not use \'tls://\' in the hostname for Redis, use this option instead.';
$string['password'] = 'Password';
$string['password_help'] = 'This sets the password of the Redis server.';
$string['pluginname'] = 'Redis';
$string['prefix'] = 'Key prefix';
$string['prefix_help'] = 'This prefix is used for all key names on the Redis server.
* If you only have one Moodle instance using this server, you can leave this value default.
* Due to key length restrictions, a maximum of 5 characters is permitted.';
$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
$string['privacy:metadata:redis'] = 'The Redis cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Redis server where data is regularly removed.';
$string['privacy:metadata:redis:data'] = 'The various data stored in the cache';
$string['serializer_igbinary'] = 'The igbinary serializer.';
$string['serializer_php'] = 'The default PHP serializer.';
$string['server'] = 'Server(s)';
$string['server_help'] = 'Redis server to use for testing.
Some example values:
* testredis.abc.com - To connect to a Redis server by hostname (Port 6379 by default).
* testredis.abc.com:1234 - To connect to a Redis server by hostname with a specific port.
* 1.2.3.4 - To connect to a Redis server by IP address (Port 6379 by default).
* 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port.
* unix:///var/redis.sock - To connect to a Redis server using a Unix socket.
* /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format).
If cluster mode is enabled, specify servers separated by a new line, for example:<br>
172.23.0.11<br>
172.23.0.12<br>
172.23.0.13<br>
For further information, see <a href="https://redis.io/docs/reference/clients/#accepting-client-connections">Accepting Client Connections</a> and <a href="https://redis.io/resources/clients/#php">Redis PHP clients</a>.';
$string['task_ttl'] = 'Free up memory used by expired entries in Redis caches';
$string['test_clustermode'] = 'Cluster mode';
$string['test_clustermode_desc'] = 'Enable Test in Redis Cluster mode.';
$string['test_password'] = 'Test server password';
$string['test_password_desc'] = 'Redis test server password.';
$string['test_serializer'] = 'Serializer';
$string['test_serializer_desc'] = 'Serializer to use for testing.';
$string['test_server'] = 'Test server';
$string['test_server_desc'] = 'Redis server to use for testing.
Some example values:
* testredis.abc.com - To connect to a Redis server by hostname (Port 6379 by default).
* testredis.abc.com:1234 - To connect to a Redis server by hostname with a specific port.
* 1.2.3.4 - To connect to a Redis server by IP address (Port 6379 by default).
* 1.2.3.4:1234 - To connect to a Redis server by IP address with a specific port.
* unix:///var/redis.sock - To connect to a Redis server using a Unix socket.
* /var/redis.sock - To connect to a Redis server using a Unix socket (alternative format).
If cluster mode is enabled, specify servers separated by a new line, for example:<br>
172.23.0.11<br>
172.23.0.12<br>
172.23.0.13<br>
For further information, see <a href="https://redis.io/docs/reference/clients/#accepting-client-connections">Accepting Client Connections</a> and <a href="https://redis.io/resources/clients/#php">Redis PHP clients</a>.';
$string['test_ttl'] = 'Testing TTL';
$string['test_ttl_desc'] = 'Run the performance test using a cache that requires TTL (slower sets).';
$string['usecompressor'] = 'Use compressor';
$string['usecompressor_help'] = 'Specifies the compressor to use after serializing. It is done at Moodle Cache API level, not at php-redis level.';
$string['useserializer'] = 'Use serializer';
$string['useserializer_help'] = 'Specifies the serializer to use for serializing.
The valid serializers are Redis::SERIALIZER_PHP or Redis::SERIALIZER_IGBINARY.
The latter is supported only when phpredis is configured with --enable-redis-igbinary option and the igbinary extension is loaded.';
+1070
View File
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redis Cache Store - Settings
*
* @package cachestore_redis
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$settings->add(
new admin_setting_configcheckbox(
name: 'cachestore_redis/test_clustermode',
visiblename: get_string('clustermode', 'cachestore_redis'),
description: cache_helper::is_cluster_available() ?
get_string('clustermode_help', 'cachestore_redis') :
get_string('clustermodeunavailable', 'cachestore_redis'),
defaultsetting: 0,
)
);
$settings->add(
new admin_setting_configtextarea(
name: 'cachestore_redis/test_server',
visiblename: get_string('test_server', 'cachestore_redis'),
description: get_string('test_server_desc', 'cachestore_redis'),
defaultsetting: '',
paramtype: PARAM_TEXT,
)
);
$settings->add(new admin_setting_configcheckbox(
'cachestore_redis/test_encryption',
get_string('encrypt_connection', 'cachestore_redis'),
get_string('encrypt_connection', 'cachestore_redis'),
false));
$settings->add(
new admin_setting_configtext(
'cachestore_redis/test_cafile',
get_string('ca_file', 'cachestore_redis'),
get_string('ca_file', 'cachestore_redis'),
'',
PARAM_TEXT,
16
)
);
$settings->add(
new admin_setting_configpasswordunmask(
'cachestore_redis/test_password',
get_string('test_password', 'cachestore_redis'),
get_string('test_password_desc', 'cachestore_redis'),
''
)
);
if (class_exists('Redis')) { // Only if Redis is available.
$options = array(Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis'));
if (defined('Redis::SERIALIZER_IGBINARY')) {
$options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis');
}
$settings->add(new admin_setting_configselect(
'cachestore_redis/test_serializer',
get_string('test_serializer', 'cachestore_redis'),
get_string('test_serializer_desc', 'cachestore_redis'),
Redis::SERIALIZER_PHP,
$options
)
);
}
$settings->add(new admin_setting_configcheckbox(
'cachestore_redis/test_ttl',
get_string('test_ttl', 'cachestore_redis'),
get_string('test_ttl_desc', 'cachestore_redis'),
false));
@@ -0,0 +1,193 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis;
use cache_definition;
use cache_store;
use cachestore_redis;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../../tests/fixtures/stores.php');
require_once(__DIR__ . '/../lib.php');
/**
* Redis cluster test.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file:
*
* define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', 'localhost:7000,localhost:7001');
* define('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER', true);
* define('TEST_CACHESTORE_REDIS_AUTHCLUSTER', 'foobared');
* define('TEST_CACHESTORE_REDIS_CASCLUSTER', '/cafile/dir/ca.crt');
*
* @package cachestore_redis
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2017 Catalyst IT Australia {@link http://www.catalyst-au.net}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @coversDefaultClass \cachestore_redis
*/
class cachestore_cluster_redis_test extends \advanced_testcase {
/**
* Create a cache store for testing the Redis cluster.
*
* @param string|null $seed The redis cluster servers.
* @return cachestore_redis The created cache store instance.
*/
public function create_store(?string $seed = null): cachestore_redis {
global $DB;
$definition = cache_definition::load_adhoc(
mode: cache_store::MODE_APPLICATION,
component: 'cachestore_redis',
area: 'phpunit_test',
);
$servers = $seed ?? str_replace(",", "\n", TEST_CACHESTORE_REDIS_SERVERSCLUSTER);
$config = [
'server' => $servers,
'prefix' => $DB->get_prefix(),
'clustermode' => true,
];
if (defined('TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER') && TEST_CACHESTORE_REDIS_ENCRYPTCLUSTER === true) {
$config['encryption'] = true;
}
if (defined('TEST_CACHESTORE_REDIS_AUTHCLUSTER') && TEST_CACHESTORE_REDIS_AUTHCLUSTER) {
$config['password'] = TEST_CACHESTORE_REDIS_AUTHCLUSTER;
}
if (defined('TEST_CACHESTORE_REDIS_CASCLUSTER') && TEST_CACHESTORE_REDIS_CASCLUSTER) {
$config['cafile'] = TEST_CACHESTORE_REDIS_CASCLUSTER;
}
$store = new cachestore_redis('TestCluster', $config);
$store->initialise($definition);
$store->purge();
return $store;
}
/**
* Set up the test environment.
*/
public function setUp(): void {
if (!cachestore_redis::are_requirements_met()) {
$this->markTestSkipped('Could not test cachestore_redis with cluster, missing requirements.');
} else if (!\cache_helper::is_cluster_available()) {
$this->markTestSkipped('Could not test cachestore_redis with cluster, class RedisCluster is not available.');
} else if (!defined('TEST_CACHESTORE_REDIS_SERVERSCLUSTER')) {
$this->markTestSkipped('Could not test cachestore_redis with cluster, missing configuration. ' .
"Example: define('TEST_CACHESTORE_REDIS_SERVERSCLUSTER', " .
"'localhost:7000,localhost:7001,localhost:7002');");
}
}
/**
* Test if the cache store can be created successfully.
*
* @covers ::is_ready
*/
public function test_it_can_create(): void {
$store = $this->create_store();
$this->assertNotNull($store);
$this->assertTrue($store->is_ready());
}
/**
* Test if the cache store trims server names correctly.
*
* @covers ::new_redis
*/
public function test_it_trims_server_names(): void {
// Add a time before and spaces after the first server. Also adds a blank line before second server.
$servers = explode(',', TEST_CACHESTORE_REDIS_SERVERSCLUSTER);
$servers[0] = "\t" . $servers[0] . " \n";
$servers = implode("\n", $servers);
$store = $this->create_store($servers);
$this->assertTrue($store->is_ready());
}
/**
* Test if the cache store can successfully set and get a value.
*
* @covers ::set
* @covers ::get
*/
public function test_it_can_setget(): void {
$store = $this->create_store();
$store->set('the key', 'the value');
$actual = $store->get('the key');
$this->assertSame('the value', $actual);
}
/**
* Test if the cache store can successfully set and get multiple values.
*
* @covers ::set_many
* @covers ::get_many
*/
public function test_it_can_setget_many(): void {
$store = $this->create_store();
// Create values.
$values = [];
$keys = [];
$expected = [];
for ($i = 0; $i < 10; $i++) {
$key = "getkey_{$i}";
$value = "getvalue #{$i}";
$keys[] = $key;
$values[] = [
'key' => $key,
'value' => $value,
];
$expected[$key] = $value;
}
$store->set_many($values);
$actual = $store->get_many($keys);
$this->assertSame($expected, $actual);
}
/**
* Test if the cache store is marked as not ready if it fails to connect.
*
* @covers ::is_ready
*/
public function test_it_is_marked_not_ready_if_failed_to_connect(): void {
global $DB;
$config = [
'server' => "abc:123",
'prefix' => $DB->get_prefix(),
'clustermode' => true,
];
$store = new cachestore_redis('TestCluster', $config);
$debugging = $this->getDebuggingMessages();
// Failed to connect should show a debugging message.
$this->assertCount(1, \phpunit_util::get_debugging_messages() );
$this->assertStringContainsString('Couldn\'t map cluster keyspace using any provided seed', $debugging[0]->message);
$this->resetDebugging();
$this->assertFalse($store->is_ready());
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis;
use cache_definition;
use cache_store;
use cachestore_redis;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/../../../tests/fixtures/stores.php');
require_once(__DIR__.'/../lib.php');
/**
* Redis cache store test.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file.
*
* define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
*
* @package cachestore_redis
* @copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
* @coversDefaultClass \cachestore_redis
*/
class cachestore_redis_test extends \cachestore_tests {
/** @var cachestore_redis $store Redis Cache Store. */
protected $store;
/**
* Returns the class name.
*
* @return string
*/
protected function get_class_name(): string {
return 'cachestore_redis';
}
public function setUp(): void {
if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
}
parent::setUp();
}
protected function tearDown(): void {
parent::tearDown();
if ($this->store instanceof cachestore_redis) {
$this->store->purge();
}
}
/**
* Creates the required cachestore for the tests to run against Redis.
*
* @return cachestore_redis
*/
protected function create_cachestore_redis(): cachestore_redis {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
$store = new cachestore_redis('Test', cachestore_redis::unit_test_configuration());
$store->initialise($definition);
$this->store = $store;
$store->purge();
return $store;
}
/**
* Test methods for various operations (set and has) in the cachestore_redis class.
*
* @covers ::set
* @covers ::has
*/
public function test_has(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->has('foo'));
$this->assertFalse($store->has('bat'));
}
/**
* Test methods for the 'has_any' operation in the cachestore_redis class.
*
* @covers ::set
* @covers ::has_any
*/
public function test_has_any(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->has_any(['bat', 'foo']));
$this->assertFalse($store->has_any(['bat', 'baz']));
}
/**
* PHPUnit test methods for the 'has_all' operation in the cachestore_redis class.
*
* @covers ::set
* @covers ::has_all
*/
public function test_has_all(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->set('bat', 'baz'));
$this->assertTrue($store->has_all(['foo', 'bat']));
$this->assertFalse($store->has_all(['foo', 'bat', 'this']));
}
/**
* Test methods for the 'lock' operations in the cachestore_redis class.
*
* @covers ::acquire_lock
* @covers ::check_lock_state
* @covers ::release_lock
*/
public function test_lock(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->acquire_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '123'));
$this->assertFalse($store->check_lock_state('lock', '321'));
$this->assertNull($store->check_lock_state('notalock', '123'));
$this->assertFalse($store->release_lock('lock', '321'));
$this->assertTrue($store->release_lock('lock', '123'));
}
/**
* Test method to check if the cachestore_redis instance is ready after connecting.
*
* @covers ::is_ready
*/
public function test_it_is_ready_after_connecting(): void {
$store = $this->create_cachestore_redis();
$this::assertTrue($store->is_ready());
}
}
+288
View File
@@ -0,0 +1,288 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis;
use cache_definition;
use cache_store;
use cachestore_redis;
require_once(__DIR__.'/../../../tests/fixtures/stores.php');
require_once(__DIR__.'/../lib.php');
/**
* Redis cache test - compressor settings.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file.
*
* define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
*
* @package cachestore_redis
* @author Daniel Thee Roperto <daniel.roperto@catalyst-au.net>
* @copyright 2018 Catalyst IT Australia {@link http://www.catalyst-au.net}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class compressor_test extends \advanced_testcase {
/**
* Test set up
*/
public function setUp(): void {
if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
}
parent::setUp();
}
/**
* Create a cachestore.
*
* @param int $compressor
* @param int $serializer
* @return cachestore_redis
*/
public function create_store($compressor, $serializer) {
/** @var cache_definition $definition */
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
$config = cachestore_redis::unit_test_configuration();
$config['compressor'] = $compressor;
$config['serializer'] = $serializer;
$store = new cachestore_redis('Test', $config);
$store->initialise($definition);
return $store;
}
/**
* It misses a value.
*/
public function test_it_can_miss_one(): void {
$store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP);
self::assertFalse($store->get('missme'));
}
/**
* It misses many values.
*/
public function test_it_can_miss_many(): void {
$store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP);
$expected = ['missme' => false, 'missmetoo' => false];
$actual = $store->get_many(array_keys($expected));
self::assertSame($expected, $actual);
}
/**
* It misses some values.
*/
public function test_it_can_miss_some(): void {
$store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP);
$store->set('iamhere', 'youfoundme');
$expected = ['missme' => false, 'missmetoo' => false, 'iamhere' => 'youfoundme'];
$actual = $store->get_many(array_keys($expected));
self::assertSame($expected, $actual);
}
/**
* A provider for test_works_with_different_types
*
* @return array
*/
public function provider_for_test_it_works_with_different_types() {
$object = new \stdClass();
$object->field = 'value';
return [
['string', 'Abc Def'],
['string_empty', ''],
['string_binary', gzencode('some binary data')],
['int', 123],
['int_zero', 0],
['int_negative', -100],
['int_huge', PHP_INT_MAX],
['float', 3.14],
['boolean_true', true],
// Boolean 'false' is not tested as it is not allowed in Moodle.
['array', [1, 'b', 3.4]],
['array_map', ['a' => 'b', 'c' => 'd']],
['object_stdClass', $object],
['null', null],
];
}
/**
* It works with different types.
*
* @dataProvider provider_for_test_it_works_with_different_types
* @param string $key
* @param mixed $value
*/
public function test_it_works_with_different_types($key, $value): void {
$store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP);
$store->set($key, $value);
self::assertEquals($value, $store->get($key), "Failed set/get for: {$key}");
}
/**
* Test it works with different types for many.
*/
public function test_it_works_with_different_types_for_many(): void {
$store = $this->create_store(cachestore_redis::COMPRESSOR_PHP_GZIP, \Redis::SERIALIZER_PHP);
$provider = $this->provider_for_test_it_works_with_different_types();
$keys = [];
$values = [];
$expected = [];
foreach ($provider as $item) {
$keys[] = $item[0];
$values[] = ['key' => $item[0], 'value' => $item[1]];
$expected[$item[0]] = $item[1];
}
$store->set_many($values);
$actual = $store->get_many($keys);
self::assertEquals($expected, $actual);
}
/**
* Provider for set/get combination tests.
*
* @return array
*/
public function provider_for_tests_setget() {
if (!cachestore_redis::are_requirements_met()) {
// Even though we skip all tests in this case, this provider can still show warnings about non-existing class.
return [];
}
$data = [
['none, none',
\Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_NONE,
'value1', 'value2'],
['none, gzip',
\Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_GZIP,
gzencode('value1'), gzencode('value2')],
['php, none',
\Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_NONE,
serialize('value1'), serialize('value2')],
['php, gzip',
\Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_GZIP,
gzencode(serialize('value1')), gzencode(serialize('value2'))],
];
if (defined('Redis::SERIALIZER_IGBINARY')) {
$data[] = [
'igbinary, none',
\Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_NONE,
igbinary_serialize('value1'), igbinary_serialize('value2'),
];
$data[] = [
'igbinary, gzip',
\Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_GZIP,
gzencode(igbinary_serialize('value1')), gzencode(igbinary_serialize('value2')),
];
}
if (extension_loaded('zstd')) {
$data[] = [
'none, zstd',
\Redis::SERIALIZER_NONE, cachestore_redis::COMPRESSOR_PHP_ZSTD,
zstd_compress('value1'), zstd_compress('value2'),
];
$data[] = [
'php, zstd',
\Redis::SERIALIZER_PHP, cachestore_redis::COMPRESSOR_PHP_ZSTD,
zstd_compress(serialize('value1')), zstd_compress(serialize('value2')),
];
if (defined('\Redis::SERIALIZER_IGBINARY')) {
$data[] = [
'igbinary, zstd',
\Redis::SERIALIZER_IGBINARY, cachestore_redis::COMPRESSOR_PHP_ZSTD,
zstd_compress(igbinary_serialize('value1')), zstd_compress(igbinary_serialize('value2')),
];
}
}
return $data;
}
/**
* Test we can use get and set with all combinations.
*
* @dataProvider provider_for_tests_setget
* @param string $name
* @param int $serializer
* @param int $compressor
* @param string $rawexpected1
* @param string $rawexpected2
*/
public function test_it_can_use_getset($name, $serializer, $compressor, $rawexpected1, $rawexpected2): void {
// Create a connection with the desired serialisation.
$store = $this->create_store($compressor, $serializer);
$store->set('key', 'value1');
// Disable compressor and serializer to check the actual stored value.
$rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, \Redis::SERIALIZER_NONE);
$data = $store->get('key');
$rawdata = $rawstore->get('key');
self::assertSame('value1', $data, "Invalid serialisation/unserialisation for: {$name}");
self::assertSame($rawexpected1, $rawdata, "Invalid rawdata for: {$name}");
}
/**
* Test we can use get and set many with all combinations.
*
* @dataProvider provider_for_tests_setget
* @param string $name
* @param int $serializer
* @param int $compressor
* @param string $rawexpected1
* @param string $rawexpected2
*/
public function test_it_can_use_getsetmany($name, $serializer, $compressor, $rawexpected1, $rawexpected2): void {
$many = [
['key' => 'key1', 'value' => 'value1'],
['key' => 'key2', 'value' => 'value2'],
];
$keys = ['key1', 'key2'];
$expectations = ['key1' => 'value1', 'key2' => 'value2'];
$rawexpectations = ['key1' => $rawexpected1, 'key2' => $rawexpected2];
// Create a connection with the desired serialisation.
$store = $this->create_store($compressor, $serializer);
$store->set_many($many);
// Disable compressor and serializer to check the actual stored value.
$rawstore = $this->create_store(cachestore_redis::COMPRESSOR_NONE, \Redis::SERIALIZER_NONE);
$data = $store->get_many($keys);
$rawdata = $rawstore->get_many($keys);
foreach ($keys as $key) {
self::assertSame($expectations[$key],
$data[$key],
"Invalid serialisation/unserialisation for {$key} with serializer {$name}");
self::assertSame($rawexpectations[$key],
$rawdata[$key],
"Invalid rawdata for {$key} with serializer {$name}");
}
}
}
+275
View File
@@ -0,0 +1,275 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis;
use cache_store;
use cache_definition;
use cachestore_redis;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__.'/../../../tests/fixtures/stores.php');
require_once(__DIR__.'/../lib.php');
/**
* Redis cache test.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file.
*
* define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
*
* @package cachestore_redis
* @covers \cachestore_redis
* @copyright Copyright (c) 2015 Moodlerooms Inc. (http://www.moodlerooms.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \cachestore_tests {
/**
* @var cachestore_redis
*/
protected $store;
/**
* Returns the MongoDB class name
*
* @return string
*/
protected function get_class_name() {
return 'cachestore_redis';
}
public function setUp(): void {
if (!cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
}
parent::setUp();
}
protected function tearDown(): void {
parent::tearDown();
if ($this->store instanceof cachestore_redis) {
$this->store->purge();
}
}
/**
* Creates the required cachestore for the tests to run against Redis.
*
* @param array $extraconfig Extra configuration options for Redis instance, if any
* @param bool $ttl True to use a cache definition with TTL enabled
* @return cachestore_redis
*/
protected function create_cachestore_redis(array $extraconfig = [], bool $ttl = false): cachestore_redis {
if ($ttl) {
/** @var cache_definition $definition */
$definition = cache_definition::load('core/wibble', [
'mode' => 1,
'simplekeys' => true,
'simpledata' => true,
'ttl' => 10,
'component' => 'core',
'area' => 'wibble',
'selectedsharingoption' => 2,
'userinputsharingkey' => '',
'sharingoptions' => 15,
]);
} else {
/** @var cache_definition $definition */
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_redis', 'phpunit_test');
}
$configuration = array_merge(cachestore_redis::unit_test_configuration(), $extraconfig);
$store = new cachestore_redis('Test', $configuration);
$store->initialise($definition);
$this->store = $store;
if (!$store) {
$this->markTestSkipped();
}
return $store;
}
public function test_has(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->has('foo'));
$this->assertFalse($store->has('bat'));
}
public function test_has_any(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->has_any(array('bat', 'foo')));
$this->assertFalse($store->has_any(array('bat', 'baz')));
}
public function test_has_all(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->set('foo', 'bar'));
$this->assertTrue($store->set('bat', 'baz'));
$this->assertTrue($store->has_all(array('foo', 'bat')));
$this->assertFalse($store->has_all(array('foo', 'bat', 'this')));
}
public function test_lock(): void {
$store = $this->create_cachestore_redis();
$this->assertTrue($store->acquire_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '123'));
$this->assertFalse($store->check_lock_state('lock', '321'));
$this->assertNull($store->check_lock_state('notalock', '123'));
$this->assertFalse($store->release_lock('lock', '321'));
$this->assertTrue($store->release_lock('lock', '123'));
}
/**
* Checks the timeout features of locking.
*/
public function test_lock_timeouts(): void {
$store = $this->create_cachestore_redis(['lockwait' => 2, 'locktimeout' => 4]);
// User 123 acquires lock.
$this->assertTrue($store->acquire_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '123'));
// User 456 tries to acquire lock - should fail after about 2 seconds.
$before = microtime(true);
$this->assertFalse($store->acquire_lock('lock', '456'));
$after = microtime(true);
$this->assertEqualsWithDelta(2, $after - $before, 0.5);
// Wait another 2 seconds and then it should be able to get the lock because of timeout.
sleep(2);
$this->assertTrue($store->acquire_lock('lock', '456'));
$this->assertTrue($store->check_lock_state('lock', '456'));
// The first user doesn't have the lock any more.
$this->assertFalse($store->check_lock_state('lock', '123'));
// Releasing the lock from the first user does nothing.
$this->assertFalse($store->release_lock('lock', '123'));
$this->assertTrue($store->check_lock_state('lock', '456'));
$this->assertTrue($store->release_lock('lock', '456'));
}
/**
* Tests the shutdown function that is supposed to free any remaining locks.
*/
public function test_lock_shutdown(): void {
$store = $this->create_cachestore_redis();
try {
$this->assertTrue($store->acquire_lock('a', '123'));
$this->assertTrue($store->acquire_lock('b', '123'));
$this->assertTrue($store->acquire_lock('c', '123'));
$this->assertTrue($store->check_lock_state('a', '123'));
$this->assertTrue($store->check_lock_state('b', '123'));
$this->assertTrue($store->check_lock_state('c', '123'));
} finally {
$store->shutdown_release_locks();
$this->assertDebuggingCalledCount(3);
}
$this->assertNull($store->check_lock_state('a', '123'));
$this->assertNull($store->check_lock_state('b', '123'));
$this->assertNull($store->check_lock_state('c', '123'));
}
/**
* Tests the get_last_io_bytes function when not using compression (just returns unknown).
*/
public function test_get_last_io_bytes(): void {
$store = $this->create_cachestore_redis();
$store->set('foo', [1, 2, 3, 4]);
$this->assertEquals(\cache_store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes());
$store->get('foo');
$this->assertEquals(\cache_store::IO_BYTES_NOT_SUPPORTED, $store->get_last_io_bytes());
}
/**
* Tests the get_last_io_bytes byte count when using compression.
*/
public function test_get_last_io_bytes_compressed(): void {
$store = $this->create_cachestore_redis(['compressor' => cachestore_redis::COMPRESSOR_PHP_GZIP]);
$alphabet = 'abcdefghijklmnopqrstuvwxyz';
$store->set('small', $alphabet);
$store->set('large', str_repeat($alphabet, 10));
$store->get('small');
// Interesting 'compression'.
$this->assertEquals(54, $store->get_last_io_bytes());
$store->get('large');
// This one is actually smaller than uncompressed value!
$this->assertEquals(57, $store->get_last_io_bytes());
$store->get_many(['small', 'large']);
$this->assertEquals(111, $store->get_last_io_bytes());
$store->set('small', str_repeat($alphabet, 2));
$this->assertEquals(56, $store->get_last_io_bytes());
$store->set_many([
['key' => 'small', 'value' => $alphabet],
['key' => 'large', 'value' => str_repeat($alphabet, 10)]
]);
$this->assertEquals(111, $store->get_last_io_bytes());
}
/**
* Data provider for whether cache uses TTL or not.
*
* @return array Array with true and false options
*/
public static function ttl_or_not(): array {
return [
[false],
[true]
];
}
/**
* Tests the delete_many function.
*
* The behaviour is different with TTL enabled so we need to test with that kind of definition
* as well as a 'normal' one.
*
* @param bool $ttl True to test using a TTL definition
* @dataProvider ttl_or_not
*/
public function test_delete_many(bool $ttl): void {
$store = $this->create_cachestore_redis([], $ttl);
// Check it works to delete selected items.
$store->set('foo', 'frog');
$store->set('bar', 'amphibian');
$store->set('hmm', 'undead');
$this->store->delete_many(['foo', 'bar']);
$this->assertFalse($store->get('foo'));
$this->assertFalse($store->get('bar'));
$this->assertEquals('undead', $store->get('hmm'));
// If called with no keys it should do nothing.
$store->delete_many([]);
$this->assertEquals('undead', $store->get('hmm'));
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_redis;
/**
* TTL support test for Redis cache.
*
* If you wish to use these unit tests all you need to do is add the following definition to
* your config.php file.
*
* define('TEST_CACHESTORE_REDIS_TESTSERVERS', '127.0.0.1');
*
* @package cachestore_redis
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \cachestore_redis
*/
final class ttl_test extends \advanced_testcase {
/** @var \cachestore_redis|null Cache store */
protected $store = null;
public function setUp(): void {
// Make sure cachestore_redis is available.
require_once(__DIR__ . '/../lib.php');
if (!\cachestore_redis::are_requirements_met() || !defined('TEST_CACHESTORE_REDIS_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_redis. Requirements are not met.');
}
// Set up a Redis store with a fake definition that has TTL set to 10 seconds.
$definition = \cache_definition::load('core/wibble', [
'mode' => 1,
'simplekeys' => true,
'simpledata' => true,
'ttl' => 10,
'component' => 'core',
'area' => 'wibble',
'selectedsharingoption' => 2,
'userinputsharingkey' => '',
'sharingoptions' => 15,
]);
$this->store = new \cachestore_redis('Test', \cachestore_redis::unit_test_configuration());
$this->store->initialise($definition);
parent::setUp();
}
protected function tearDown(): void {
parent::tearDown();
if ($this->store instanceof \cachestore_redis) {
$this->store->purge();
}
}
/**
* Test calling set_many with an empty array
*
* Trivial test to ensure we don't trigger an ArgumentCountError when calling zAdd with invalid parameters
*/
public function test_set_many_empty(): void {
$this->assertEquals(0, $this->store->set_many([]));
}
/**
* Tests expiring data.
*/
public function test_expire_ttl(): void {
$this->resetAfterTest();
// Set some data at time 100.
\cachestore_redis::set_phpunit_time(100);
$this->store->set('a', 1);
$this->store->set('b', 2);
$this->store->set_many([['key' => 'c', 'value' => 3], ['key' => 'd', 'value' => 4],
['key' => 'e', 'value' => 5], ['key' => 'f', 'value' => 6],
['key' => 'g', 'value' => 7], ['key' => 'h', 'value' => 8]]);
// Set some other data at time 110, including some of the existing values. Whether the
// value changes or not, its TTL should update.
\cachestore_redis::set_phpunit_time(110);
$this->store->set('b', 2);
$this->store->set_many([['key' => 'c', 'value' => 99], ['key' => 'd', 'value' => 4]]);
// Check all the data is still set.
$this->assertEqualsCanonicalizing(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
$this->store->find_all());
// Delete some data (to check deletion doesn't confuse expiry).
$this->store->delete('f');
$this->store->delete_many(['g', 'h']);
// Set time to 115 and expire data.
\cachestore_redis::set_phpunit_time(115);
$info = $this->store->expire_ttl();
// We are expecting keys a and e to be deleted.
$this->assertEquals(2, $info['keys']);
$this->assertEquals(1, $info['batches']);
// Check the keys are as expected.
$this->assertEqualsCanonicalizing(['b', 'c', 'd'], $this->store->find_all());
// Might as well check the values of the surviving keys.
$this->assertEquals(2, $this->store->get('b'));
$this->assertEquals(99, $this->store->get('c'));
$this->assertEquals(4, $this->store->get('d'));
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Redis Cache Store - Version information
*
* @package cachestore_redis
* @copyright 2013 Adam Durana
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->maturity = MATURITY_STABLE;
$plugin->component = 'cachestore_redis';
+107
View File
@@ -0,0 +1,107 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachestore_session.
*
* @package cachestore_session
* @category privacy
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachestore_session\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\userlist;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachestore_session.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\core_userlist_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_subsystem_link('core_user', [], 'privacy:metadata:core_user');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
return new contextlist();
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
}
/**
* Delete all use data which matches the specified deletion_criteria.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the session cache store.
*
* This file is part of the session cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_session
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Session cache';
$string['privacy:metadata:core_user'] = 'The Session cachestore plugin stores data briefly as part of its caching functionality. This data is stored in the short-lived user session.';
+595
View File
@@ -0,0 +1,595 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the session cache store.
*
* This file is part of the session cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_session
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The session data store class.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class session_data_store extends cache_store {
/**
* Used for the actual storage.
* @var array
*/
private static $sessionstore = null;
/**
* Returns a static store by reference... REFERENCE SUPER IMPORTANT.
*
* @param string $id
* @return array
*/
protected static function &register_store_id($id) {
if (is_null(self::$sessionstore)) {
global $SESSION;
if (!isset($SESSION->cachestore_session)) {
$SESSION->cachestore_session = array();
}
self::$sessionstore =& $SESSION->cachestore_session;
}
if (!array_key_exists($id, self::$sessionstore)) {
self::$sessionstore[$id] = array();
}
return self::$sessionstore[$id];
}
/**
* Flushes the data belong to the given store id.
* @param string $id
*/
protected static function flush_store_by_id($id) {
unset(self::$sessionstore[$id]);
self::$sessionstore[$id] = array();
}
/**
* Flushes the store of all data.
*/
protected static function flush_store() {
$ids = array_keys(self::$sessionstore);
unset(self::$sessionstore);
self::$sessionstore = array();
foreach ($ids as $id) {
self::$sessionstore[$id] = array();
}
}
}
/**
* The Session store class.
*
* @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, cache_is_searchable {
/**
* The name of the store
* @var store
*/
protected $name;
/**
* The store id (should be unique)
* @var string
*/
protected $storeid;
/**
* The store we use for data.
* @var array
*/
protected $store;
/**
* The ttl if there is one. Hopefully not.
* @var int
*/
protected $ttl = 0;
/**
* The maximum size for the store, or false if there isn't one.
* @var bool|int
*/
protected $maxsize = false;
/**
* The number of items currently being stored.
* @var int
*/
protected $storecount = 0;
/**
* Constructs the store instance.
*
* Noting that this function is not an initialisation. It is used to prepare the store for use.
* The store will be initialised when required and will be provided with a cache_definition at that time.
*
* @param string $name
* @param array $configuration
*/
public function __construct($name, array $configuration = array()) {
$this->name = $name;
}
/**
* Returns the supported features as a combined int.
*
* @param array $configuration
* @return int
*/
public static function get_supported_features(array $configuration = array()) {
return self::SUPPORTS_DATA_GUARANTEE +
self::SUPPORTS_NATIVE_TTL +
self::IS_SEARCHABLE;
}
/**
* Returns false as this store does not support multiple identifiers.
* (This optional function is a performance optimisation; it must be
* consistent with the value from get_supported_features.)
*
* @return bool False
*/
public function supports_multiple_identifiers() {
return false;
}
/**
* Returns the supported modes as a combined int.
*
* @param array $configuration
* @return int
*/
public static function get_supported_modes(array $configuration = array()) {
return self::MODE_SESSION;
}
/**
* Returns true if the store requirements are met.
*
* @return bool
*/
public static function are_requirements_met() {
return true;
}
/**
* Returns true if the given mode is supported by this store.
*
* @param int $mode One of cache_store::MODE_*
* @return bool
*/
public static function is_supported_mode($mode) {
return ($mode === self::MODE_SESSION);
}
/**
* Initialises the cache.
*
* Once this has been done the cache is all set to be used.
*
* @param cache_definition $definition
*/
public function initialise(cache_definition $definition) {
$this->storeid = $definition->generate_definition_hash();
$this->store = &self::register_store_id($this->name.'-'.$definition->get_id());
$this->ttl = $definition->get_ttl();
$maxsize = $definition->get_maxsize();
if ($maxsize !== null) {
// Must be a positive int.
$this->maxsize = abs((int)$maxsize);
$this->storecount = count($this->store);
}
$this->check_ttl();
}
/**
* Returns true once this instance has been initialised.
*
* @return bool
*/
public function is_initialised() {
return (is_array($this->store));
}
/**
* 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) {
if (isset($this->store[$key])) {
if ($this->ttl == 0) {
$value = $this->store[$key][0];
if ($this->maxsize !== false) {
// Make sure the element is now in the end of array.
$this->set($key, $value);
}
return $value;
} else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
return $this->store[$key][0];
} else {
// Element is present but has expired.
$this->check_ttl();
}
}
return false;
}
/**
* 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) {
$return = array();
$maxtime = 0;
if ($this->ttl != 0) {
$maxtime = cache::now() - $this->ttl;
}
$hasexpiredelements = false;
foreach ($keys as $key) {
$return[$key] = false;
if (isset($this->store[$key])) {
if ($this->ttl == 0) {
$return[$key] = $this->store[$key][0];
if ($this->maxsize !== false) {
// Make sure the element is now in the end of array.
$this->set($key, $return[$key], false);
}
} else if ($this->store[$key][1] >= $maxtime) {
$return[$key] = $this->store[$key][0];
} else {
$hasexpiredelements = true;
}
}
}
if ($hasexpiredelements) {
// There are some elements that are present but have expired.
$this->check_ttl();
}
return $return;
}
/**
* 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.
* @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. If this is set to false you will
* need to perform these checks yourself. This allows for bulk set's to be performed and maxsize tests performed once.
* @return bool True if the operation was a success false otherwise.
*/
public function set($key, $data, $testmaxsize = true) {
$testmaxsize = ($testmaxsize && $this->maxsize !== false);
$increment = $this->maxsize !== false && !isset($this->store[$key]);
if (($this->maxsize !== false && !$increment) || $this->ttl != 0) {
// Make sure the element is added to the end of $this->store array.
unset($this->store[$key]);
}
if ($this->ttl === 0) {
$this->store[$key] = array($data, 0);
} else {
$this->store[$key] = array($data, cache::now());
}
if ($increment) {
$this->storecount++;
}
if ($testmaxsize && $this->storecount > $this->maxsize) {
$this->reduce_for_maxsize();
}
return true;
}
/**
* 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) {
$count = 0;
$increment = 0;
foreach ($keyvaluearray as $pair) {
$key = $pair['key'];
$data = $pair['value'];
$count++;
if ($this->maxsize !== false || $this->ttl !== 0) {
// Make sure the element is added to the end of $this->store array.
$this->delete($key);
$increment++;
} else if (!isset($this->store[$key])) {
$increment++;
}
if ($this->ttl === 0) {
$this->store[$key] = array($data, 0);
} else {
$this->store[$key] = array($data, cache::now());
}
}
if ($this->maxsize !== false) {
$this->storecount += $increment;
if ($this->storecount > $this->maxsize) {
$this->reduce_for_maxsize();
}
}
return $count;
}
/**
* Checks if the store has a record for the given key and returns true if so.
*
* @param string $key
* @return bool
*/
public function has($key) {
if (isset($this->store[$key])) {
if ($this->ttl == 0) {
return true;
} else if ($this->store[$key][1] >= (cache::now() - $this->ttl)) {
return true;
}
}
return false;
}
/**
* Returns true if the store contains records for all of the given keys.
*
* @param array $keys
* @return bool
*/
public function has_all(array $keys) {
$maxtime = 0;
if ($this->ttl != 0) {
$maxtime = cache::now() - $this->ttl;
}
foreach ($keys as $key) {
if (!isset($this->store[$key])) {
return false;
}
if ($this->ttl != 0 && $this->store[$key][1] < $maxtime) {
return false;
}
}
return true;
}
/**
* Returns true if the store contains records for any of the given keys.
*
* @param array $keys
* @return bool
*/
public function has_any(array $keys) {
$maxtime = 0;
if ($this->ttl != 0) {
$maxtime = cache::now() - $this->ttl;
}
foreach ($keys as $key) {
if (isset($this->store[$key]) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) {
return true;
}
}
return false;
}
/**
* 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) {
if (!isset($this->store[$key])) {
return false;
}
unset($this->store[$key]);
if ($this->maxsize !== false) {
$this->storecount--;
}
return true;
}
/**
* 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) {
// The number of items that have actually being removed.
$reduction = 0;
foreach ($keys as $key) {
if (isset($this->store[$key])) {
$reduction++;
}
unset($this->store[$key]);
}
if ($this->maxsize !== false) {
$this->storecount -= $reduction;
}
return $reduction;
}
/**
* Purges the cache deleting all items within it.
*
* @return boolean True on success. False otherwise.
*/
public function purge() {
$this->store = array();
// Don't worry about checking if we're using max size just set it as thats as fast as the check.
$this->storecount = 0;
return true;
}
/**
* Reduces the size of the array if maxsize has been hit.
*
* This function reduces the size of the store reducing it by 10% of its maxsize.
* It removes the oldest items in the store when doing this.
* The reason it does this an doesn't use a least recently used system is purely the overhead such a system
* requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
* and avoiding more is of benefit.
*
* @return int
*/
protected function reduce_for_maxsize() {
$diff = $this->storecount - $this->maxsize;
if ($diff < 1) {
return 0;
}
// Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
$diff += floor($this->maxsize / 10);
$this->store = array_slice($this->store, $diff, null, true);
$this->storecount -= $diff;
return $diff;
}
/**
* Returns true if the user can add an instance of the store plugin.
*
* @return bool
*/
public static function can_add_instance() {
return false;
}
/**
* Performs any necessary clean up when the store instance is being deleted.
*/
public function instance_deleted() {
$this->purge();
}
/**
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
* @return cachestore_session
*/
public static function initialise_test_instance(cache_definition $definition) {
// Do something here perhaps.
$cache = new cachestore_session('Session test');
$cache->initialise($definition);
return $cache;
}
/**
* Generates the appropriate configuration required for unit testing.
*
* @return array Array of unit test configuration data to be used by initialise().
*/
public static function unit_test_configuration() {
return array();
}
/**
* Returns the name of this instance.
* @return string
*/
public function my_name() {
return $this->name;
}
/**
* Removes expired elements.
* @return int number of removed elements
*/
protected function check_ttl() {
if ($this->ttl === 0) {
return 0;
}
$maxtime = cache::now() - $this->ttl;
$count = 0;
for ($value = reset($this->store); $value !== false; $value = next($this->store)) {
if ($value[1] >= $maxtime) {
// We know that elements are sorted by ttl so no need to continue.
break;
}
$count++;
}
if ($count) {
// Remove first $count elements as they are expired.
$this->store = array_slice($this->store, $count, null, true);
if ($this->maxsize !== false) {
$this->storecount -= $count;
}
}
return $count;
}
/**
* Finds all of the keys being stored in the cache store instance.
*
* @return array
*/
public function find_all() {
$this->check_ttl();
return array_keys($this->store);
}
/**
* Finds all of the keys whose keys start with the given prefix.
*
* @param string $prefix
* @return array An array of keys.
*/
public function find_by_prefix($prefix) {
$return = array();
foreach ($this->find_all() as $key) {
if (strpos($key, $prefix) === 0) {
$return[] = $key;
}
}
return $return;
}
/**
* This store supports native TTL handling.
* @return bool
*/
public function store_supports_native_ttl() {
return true;
}
}
+195
View File
@@ -0,0 +1,195 @@
<?php
// This session is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_session;
use cache_store;
defined('MOODLE_INTERNAL') || die();
// Include the necessary evils.
global $CFG;
require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
require_once($CFG->dirroot.'/cache/stores/session/lib.php');
/**
* Session unit test class.
*
* @package cachestore_session
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \cachestore_tests {
/**
* Returns the session class name
* @return string
*/
protected function get_class_name() {
return 'cachestore_session';
}
/**
* Test the maxsize option.
*/
public function test_maxsize(): void {
$config = \cache_config_testing::instance();
$config->phpunit_add_definition('phpunit/one', array(
'mode' => cache_store::MODE_SESSION,
'component' => 'phpunit',
'area' => 'one',
'maxsize' => 3
));
$config->phpunit_add_definition('phpunit/two', array(
'mode' => cache_store::MODE_SESSION,
'component' => 'phpunit',
'area' => 'two',
'maxsize' => 3
));
$cacheone = \cache::make('phpunit', 'one');
$this->assertTrue($cacheone->set('key1', 'value1'));
$this->assertTrue($cacheone->set('key2', 'value2'));
$this->assertTrue($cacheone->set('key3', 'value3'));
$this->assertTrue($cacheone->has('key1'));
$this->assertTrue($cacheone->has('key2'));
$this->assertTrue($cacheone->has('key3'));
$this->assertTrue($cacheone->set('key4', 'value4'));
$this->assertTrue($cacheone->set('key5', 'value5'));
$this->assertFalse($cacheone->has('key1'));
$this->assertFalse($cacheone->has('key2'));
$this->assertTrue($cacheone->has('key3'));
$this->assertTrue($cacheone->has('key4'));
$this->assertTrue($cacheone->has('key5'));
$this->assertFalse($cacheone->get('key1'));
$this->assertFalse($cacheone->get('key2'));
$this->assertEquals('value3', $cacheone->get('key3'));
$this->assertEquals('value4', $cacheone->get('key4'));
$this->assertEquals('value5', $cacheone->get('key5'));
// Test adding one more.
$this->assertTrue($cacheone->set('key6', 'value6'));
$this->assertFalse($cacheone->get('key3'));
// Test reducing and then adding to make sure we don't lost one.
$this->assertTrue($cacheone->delete('key6'));
$this->assertTrue($cacheone->set('key7', 'value7'));
$this->assertEquals('value4', $cacheone->get('key4'));
// Set the same key three times to make sure it doesn't count overrides.
for ($i = 0; $i < 3; $i++) {
$this->assertTrue($cacheone->set('key8', 'value8'));
}
$this->assertEquals('value7', $cacheone->get('key7'), 'Overrides are incorrectly incrementing size');
// Test adding many.
$this->assertEquals(3, $cacheone->set_many(array(
'keyA' => 'valueA',
'keyB' => 'valueB',
'keyC' => 'valueC'
)));
$this->assertEquals(array(
'key4' => false,
'key5' => false,
'key6' => false,
'key7' => false,
'keyA' => 'valueA',
'keyB' => 'valueB',
'keyC' => 'valueC'
), $cacheone->get_many(array(
'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
)));
$cachetwo = \cache::make('phpunit', 'two');
// Test adding many.
$this->assertEquals(3, $cacheone->set_many(array(
'keyA' => 'valueA',
'keyB' => 'valueB',
'keyC' => 'valueC'
)));
$this->assertEquals(3, $cachetwo->set_many(array(
'key1' => 'value1',
'key2' => 'value2',
'key3' => 'value3'
)));
$this->assertEquals(array(
'keyA' => 'valueA',
'keyB' => 'valueB',
'keyC' => 'valueC'
), $cacheone->get_many(array(
'keyA', 'keyB', 'keyC'
)));
$this->assertEquals(array(
'key1' => 'value1',
'key2' => 'value2',
'key3' => 'value3'
), $cachetwo->get_many(array(
'key1', 'key2', 'key3'
)));
// Test that that cache deletes element that was least recently accessed.
$this->assertEquals('valueA', $cacheone->get('keyA'));
$cacheone->set('keyD', 'valueD');
$this->assertEquals('valueA', $cacheone->get('keyA'));
$this->assertFalse($cacheone->get('keyB'));
$this->assertEquals(array('keyD' => 'valueD', 'keyC' => 'valueC'), $cacheone->get_many(array('keyD', 'keyC')));
$cacheone->set('keyE', 'valueE');
$this->assertFalse($cacheone->get('keyB'));
$this->assertFalse($cacheone->get('keyA'));
$this->assertEquals(array('keyA' => false, 'keyE' => 'valueE', 'keyD' => 'valueD', 'keyC' => 'valueC'),
$cacheone->get_many(array('keyA', 'keyE', 'keyD', 'keyC')));
// Overwrite keyE (moves it to the end of array), and set keyF.
$cacheone->set_many(array('keyE' => 'valueE', 'keyF' => 'valueF'));
$this->assertEquals(array('keyC' => 'valueC', 'keyE' => 'valueE', 'keyD' => false, 'keyF' => 'valueF'),
$cacheone->get_many(array('keyC', 'keyE', 'keyD', 'keyF')));
}
public function test_ttl(): void {
$config = \cache_config_testing::instance();
$config->phpunit_add_definition('phpunit/three', array(
'mode' => cache_store::MODE_SESSION,
'component' => 'phpunit',
'area' => 'three',
'maxsize' => 3,
'ttl' => 3
));
$cachethree = \cache::make('phpunit', 'three');
// Make sure that when cache with ttl is full the elements that were added first are deleted first regardless of access time.
$cachethree->set('key1', 'value1');
$cachethree->set('key2', 'value2');
$cachethree->set('key3', 'value3');
$cachethree->set('key4', 'value4');
$this->assertFalse($cachethree->get('key1'));
$this->assertEquals('value4', $cachethree->get('key4'));
$cachethree->set('key5', 'value5');
$this->assertFalse($cachethree->get('key2'));
$this->assertEquals('value4', $cachethree->get('key4'));
$cachethree->set_many(array('key6' => 'value6', 'key7' => 'value7'));
$this->assertEquals(array('key3' => false, 'key4' => false, 'key5' => 'value5', 'key6' => 'value6', 'key7' => 'value7'),
$cachethree->get_many(array('key3', 'key4', 'key5', 'key6', 'key7')));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache session store version information.
*
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_session
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2024042200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'cachestore_session'; // Full name of the plugin.
+46
View File
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for cachestore_static.
*
* @package cachestore_static
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace cachestore_static\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for cachestore_static implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the static cache store.
*
* This file is part of the static cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_static
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'Static request cache';
$string['privacy:metadata'] = 'The Static request cachestore plugin stores some data, but this is only present for the lifetime of a single HTTP request.';
+578
View File
@@ -0,0 +1,578 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The library file for the static cache store.
*
* This file is part of the static cache store, it contains the API for interacting with an instance of the store.
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_static
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* The static data store class
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class static_data_store extends cache_store {
/**
* An array for storage.
* @var array
*/
private static $staticstore = array();
/**
* Returns a static store by reference... REFERENCE SUPER IMPORTANT.
*
* @param string $id
* @return array
*/
protected static function &register_store_id($id) {
if (!array_key_exists($id, self::$staticstore)) {
self::$staticstore[$id] = array();
}
return self::$staticstore[$id];
}
/**
* Flushes the store of all values for belonging to the store with the given id.
* @param string $id
*/
protected static function flush_store_by_id($id) {
unset(self::$staticstore[$id]);
self::$staticstore[$id] = array();
}
/**
* Flushes all of the values from all stores.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
protected static function flush_store() {
$ids = array_keys(self::$staticstore);
unset(self::$staticstore);
self::$staticstore = array();
foreach ($ids as $id) {
self::$staticstore[$id] = array();
}
}
}
/**
* The static store class.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_static extends static_data_store implements cache_is_key_aware, cache_is_searchable {
/**
* The name of the store
* @var store
*/
protected $name;
/**
* The store id (should be unique)
* @var string
*/
protected $storeid;
/**
* The store we use for data.
* @var array
*/
protected $store;
/**
* The maximum size for the store, or false if there isn't one.
* @var bool
*/
protected $maxsize = false;
/**
* Where this cache uses simpledata and we don't need to serialize it.
* @var bool
*/
protected $simpledata = false;
/**
* The number of items currently being stored.
* @var int
*/
protected $storecount = 0;
/**
* igbinary extension available.
* @var bool
*/
protected $igbinaryfound = false;
/**
* Constructs the store instance.
*
* Noting that this function is not an initialisation. It is used to prepare the store for use.
* The store will be initialised when required and will be provided with a cache_definition at that time.
*
* @param string $name
* @param array $configuration
*/
public function __construct($name, array $configuration = array()) {
$this->name = $name;
}
/**
* Returns the supported features as a combined int.
*
* @param array $configuration
* @return int
*/
public static function get_supported_features(array $configuration = array()) {
return self::SUPPORTS_DATA_GUARANTEE +
self::SUPPORTS_NATIVE_TTL +
self::IS_SEARCHABLE +
self::SUPPORTS_MULTIPLE_IDENTIFIERS +
self::DEREFERENCES_OBJECTS;
}
/**
* Returns true as this store does support multiple identifiers.
* (This optional function is a performance optimisation; it must be
* consistent with the value from get_supported_features.)
*
* @return bool true
*/
public function supports_multiple_identifiers() {
return true;
}
/**
* Returns the supported modes as a combined int.
*
* @param array $configuration
* @return int
*/
public static function get_supported_modes(array $configuration = array()) {
return self::MODE_REQUEST;
}
/**
* Returns true if the store requirements are met.
*
* @return bool
*/
public static function are_requirements_met() {
return true;
}
/**
* Returns true if the given mode is supported by this store.
*
* @param int $mode One of cache_store::MODE_*
* @return bool
*/
public static function is_supported_mode($mode) {
return ($mode === self::MODE_REQUEST);
}
/**
* Initialises the cache.
*
* Once this has been done the cache is all set to be used.
*
* @param cache_definition $definition
*/
public function initialise(cache_definition $definition) {
$keyarray = $definition->generate_multi_key_parts();
$this->storeid = $keyarray['mode'].'/'.$keyarray['component'].'/'.$keyarray['area'].'/'.$keyarray['siteidentifier'];
$this->store = &self::register_store_id($this->storeid);
$maxsize = $definition->get_maxsize();
$this->simpledata = $definition->uses_simple_data();
$this->igbinaryfound = extension_loaded('igbinary');
if ($maxsize !== null) {
// Must be a positive int.
$this->maxsize = abs((int)$maxsize);
$this->storecount = count($this->store);
}
}
/**
* Returns true once this instance has been initialised.
*
* @return bool
*/
public function is_initialised() {
return (is_array($this->store));
}
/**
* Uses igbinary serializer if igbinary extension is loaded.
* Fallback to PHP serializer.
*
* @param mixed $data
* The value to be serialized.
* @return string a string containing a byte-stream representation of
* value that can be stored anywhere.
*/
protected function serialize($data) {
if ($this->igbinaryfound) {
return igbinary_serialize($data);
} else {
return serialize($data);
}
}
/**
* Uses igbinary unserializer if igbinary extension is loaded.
* Fallback to PHP unserializer.
*
* @param string $str
* The serialized string.
* @return mixed The converted value is returned, and can be a boolean,
* integer, float, string,
* array or object.
*/
protected function unserialize($str) {
if ($this->igbinaryfound) {
return igbinary_unserialize($str);
} else {
return unserialize($str);
}
}
/**
* 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) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
if (isset($this->store[$key])) {
if ($this->store[$key]['serialized']) {
return $this->unserialize($this->store[$key]['data']);
} else {
return $this->store[$key]['data'];
}
}
return false;
}
/**
* 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) {
$return = array();
foreach ($keys as $key) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
$return[$key] = false;
if (isset($this->store[$key])) {
if ($this->store[$key]['serialized']) {
$return[$key] = $this->unserialize($this->store[$key]['data']);
} else {
$return[$key] = $this->store[$key]['data'];
}
}
}
return $return;
}
/**
* 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.
* @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required.
* @return bool True if the operation was a success false otherwise.
*/
public function set($key, $data, $testmaxsize = true) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
$testmaxsize = ($testmaxsize && $this->maxsize !== false);
if ($testmaxsize) {
$increment = (!isset($this->store[$key]));
}
if ($this->simpledata || is_scalar($data)) {
$this->store[$key]['data'] = $data;
$this->store[$key]['serialized'] = false;
} else {
$this->store[$key]['data'] = $this->serialize($data);
$this->store[$key]['serialized'] = true;
}
if ($testmaxsize && $increment) {
$this->storecount++;
if ($this->storecount > $this->maxsize) {
$this->reduce_for_maxsize();
}
}
return true;
}
/**
* 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) {
$count = 0;
foreach ($keyvaluearray as $pair) {
if (!is_array($pair['key'])) {
$pair['key'] = array('key' => $pair['key']);
}
// Don't test the maxsize here. We'll do it once when we are done.
$this->set($pair['key']['key'], $pair['value'], false);
$count++;
}
if ($this->maxsize !== false) {
$this->storecount += $count;
if ($this->storecount > $this->maxsize) {
$this->reduce_for_maxsize();
}
}
return $count;
}
/**
* Checks if the store has a record for the given key and returns true if so.
*
* @param string $key
* @return bool
*/
public function has($key) {
if (is_array($key)) {
$key = $key['key'];
}
return isset($this->store[$key]);
}
/**
* Returns true if the store contains records for all of the given keys.
*
* @param array $keys
* @return bool
*/
public function has_all(array $keys) {
foreach ($keys as $key) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
if (!isset($this->store[$key])) {
return false;
}
}
return true;
}
/**
* Returns true if the store contains records for any of the given keys.
*
* @param array $keys
* @return bool
*/
public function has_any(array $keys) {
foreach ($keys as $key) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
if (isset($this->store[$key])) {
return true;
}
}
return false;
}
/**
* 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) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
$result = isset($this->store[$key]);
unset($this->store[$key]);
if ($this->maxsize !== false) {
$this->storecount--;
}
return $result;
}
/**
* 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) {
$count = 0;
foreach ($keys as $key) {
if (!is_array($key)) {
$key = array('key' => $key);
}
$key = $key['key'];
if (isset($this->store[$key])) {
$count++;
}
unset($this->store[$key]);
}
if ($this->maxsize !== false) {
$this->storecount -= $count;
}
return $count;
}
/**
* Purges the cache deleting all items within it.
*
* @return boolean True on success. False otherwise.
*/
public function purge() {
$this->flush_store_by_id($this->storeid);
$this->store = &self::register_store_id($this->storeid);
// Don't worry about checking if we're using max size just set it as thats as fast as the check.
$this->storecount = 0;
return true;
}
/**
* Reduces the size of the array if maxsize has been hit.
*
* This function reduces the size of the store reducing it by 10% of its maxsize.
* It removes the oldest items in the store when doing this.
* The reason it does this an doesn't use a least recently used system is purely the overhead such a system
* requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches
* and avoiding more is of benefit.
*
* @return int
*/
protected function reduce_for_maxsize() {
$diff = $this->storecount - $this->maxsize;
if ($diff < 1) {
return 0;
}
// Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop.
$diff += floor($this->maxsize / 10);
$this->store = array_slice($this->store, $diff, null, true);
$this->storecount -= $diff;
return $diff;
}
/**
* Returns true if the user can add an instance of the store plugin.
*
* @return bool
*/
public static function can_add_instance() {
return false;
}
/**
* Performs any necessary clean up when the store instance is being deleted.
*/
public function instance_deleted() {
$this->purge();
}
/**
* Generates an instance of the cache store that can be used for testing.
*
* @param cache_definition $definition
* @return cachestore_static
*/
public static function initialise_test_instance(cache_definition $definition) {
// Do something here perhaps.
$cache = new cachestore_static('Static store');
$cache->initialise($definition);
return $cache;
}
/**
* Generates the appropriate configuration required for unit testing.
*
* @return array Array of unit test configuration data to be used by initialise().
*/
public static function unit_test_configuration() {
return array();
}
/**
* Returns the name of this instance.
* @return string
*/
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;
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_static;
use cache_definition;
use cache_store;
use cachestore_static;
defined('MOODLE_INTERNAL') || die();
// Include the necessary evils.
global $CFG;
require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
require_once($CFG->dirroot.'/cache/stores/static/lib.php');
/**
* Static unit test class.
*
* @package cachestore_static
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \cachestore_tests {
/**
* Returns the static class name
* @return string
*/
protected function get_class_name() {
return 'cachestore_static';
}
/**
* Test the maxsize option.
*/
public function test_maxsize(): void {
$defid = 'phpunit/testmaxsize';
$config = \cache_config_testing::instance();
$config->phpunit_add_definition($defid, array(
'mode' => cache_store::MODE_REQUEST,
'component' => 'phpunit',
'area' => 'testmaxsize',
'maxsize' => 3
));
$definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
$instance = cachestore_static::initialise_test_instance($definition);
$this->assertTrue($instance->set('key1', 'value1'));
$this->assertTrue($instance->set('key2', 'value2'));
$this->assertTrue($instance->set('key3', 'value3'));
$this->assertTrue($instance->has('key1'));
$this->assertTrue($instance->has('key2'));
$this->assertTrue($instance->has('key3'));
$this->assertTrue($instance->set('key4', 'value4'));
$this->assertTrue($instance->set('key5', 'value5'));
$this->assertFalse($instance->has('key1'));
$this->assertFalse($instance->has('key2'));
$this->assertTrue($instance->has('key3'));
$this->assertTrue($instance->has('key4'));
$this->assertTrue($instance->has('key5'));
$this->assertFalse($instance->get('key1'));
$this->assertFalse($instance->get('key2'));
$this->assertEquals('value3', $instance->get('key3'));
$this->assertEquals('value4', $instance->get('key4'));
$this->assertEquals('value5', $instance->get('key5'));
// Test adding one more.
$this->assertTrue($instance->set('key6', 'value6'));
$this->assertFalse($instance->get('key3'));
// Test reducing and then adding to make sure we don't lost one.
$this->assertTrue($instance->delete('key6'));
$this->assertTrue($instance->set('key7', 'value7'));
$this->assertEquals('value4', $instance->get('key4'));
// Set the same key three times to make sure it doesn't count overrides.
for ($i = 0; $i < 3; $i++) {
$this->assertTrue($instance->set('key8', 'value8'));
}
$this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size');
// Test adding many.
$this->assertEquals(3, $instance->set_many(array(
array('key' => 'keyA', 'value' => 'valueA'),
array('key' => 'keyB', 'value' => 'valueB'),
array('key' => 'keyC', 'value' => 'valueC')
)));
$this->assertEquals(array(
'key4' => false,
'key5' => false,
'key6' => false,
'key7' => false,
'keyA' => 'valueA',
'keyB' => 'valueB',
'keyC' => 'valueC'
), $instance->get_many(array(
'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC'
)));
}
/**
* Simple test to verify igbinary availability and check basic serialization is working ok.
*/
public function test_igbinary_serializer(): void {
// Skip if igbinary is not available.
if (!extension_loaded('igbinary')) {
$this->markTestSkipped('Cannot test igbinary serializer. Extension missing');
}
// Prepare the static instance.
$defid = 'phpunit/igbinary';
$config = \cache_config_testing::instance();
$config->phpunit_add_definition($defid, array(
'mode' => cache_store::MODE_REQUEST,
'component' => 'phpunit',
'area' => 'testigbinary'
));
$definition = cache_definition::load($defid, $config->get_definition_by_id($defid));
$instance = cachestore_static::initialise_test_instance($definition);
// Prepare an object.
$obj = new \stdClass();
$obj->someint = 9;
$obj->somestring = '99';
$obj->somearray = [9 => 999, '99' => '9999'];
// Serialize and set.
$objser = igbinary_serialize($obj);
$instance->set('testigbinary', $objser);
// Get and unserialize.
$res = $instance->get('testigbinary');
$resunser = igbinary_unserialize($res);
// Check expectations.
$this->assertSame($objser, $res); // Ok from cache (ig-serialized, 100% same string).
$this->assertEquals($obj, $resunser); // Ok ig-unserialized (equal
$this->assertNotSame($obj, $resunser);// but different objects, obviously).
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache static store version information.
*
* This is used as a default cache store within the Cache API. It should never be deleted.
*
* @package cachestore_static
* @category cache
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2024042200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2024041600; // Requires this Moodle version.
$plugin->component = 'cachestore_static'; // Full name of the plugin.
+46
View File
@@ -0,0 +1,46 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_cache/usage
The cache usage page
Context variables required for this template:
* maintable - Main table HTML
* summarytable - Summary table HTML
* samplesform - Form to choose number of samples
Example context (json):
{
"maintable": "",
"summarytable": "",
"samplesform": ""
}
}}
<h2>{{# str }} cacheusage, cache {{/ str }}</h2>
<div class="mt-6 mb-6">
<h3>{{# str }} cachestores, cache {{/ str }}</h3>
{{{summarytable}}}
</div>
<div class="mt-6 mb-6">
<h3>{{# str }} details {{/ str }}</h3>
{{{maintable}}}
</div>
{{{samplesform}}}
+209
View File
@@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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');
$session = cache_definition::load_adhoc(cache_store::MODE_SESSION, 'cache', 'sessiontest');
$request = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cache', 'requesttest');
$strinvalidplugin = new lang_string('invalidplugin', 'cache');
$strunsupportedmode = new lang_string('unsupportedmode', 'cache');
$struntestable = new lang_string('untestable', 'cache');
$strtested = new lang_string('tested', 'cache');
$strnotready = new lang_string('storenotready', 'cache');
foreach (core_component::get_plugin_list_with_file('cachestore', 'lib.php', true) as $plugin => $path) {
$class = 'cachestore_'.$plugin;
$plugin = get_string('pluginname', 'cachestore_'.$plugin);
if (!class_exists($class) || !method_exists($class, 'initialise_test_instance') || !$class::are_requirements_met()) {
$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 if (!$store->is_ready()) {
$applicationtable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
} 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->instance_deleted();
}
}
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 if (!$store->is_ready()) {
$sessiontable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
} 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->instance_deleted();
}
}
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 if (!$store->is_ready()) {
$requesttable->data[] = array($plugin, $strnotready, '-', '-', '-', '-');
} 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->instance_deleted();
}
}
}
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();
+271
View File
@@ -0,0 +1,271 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
use cache_config_testing;
use cache_config_writer;
use cache_factory;
use cache_helper;
use cache_store;
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 and in particular the core_cache\administration_helper
*
* @package core_cache
* @category test
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class administration_helper_test extends \advanced_testcase {
/**
* Set things back to the default before each test.
*/
public function setUp(): void {
parent::setUp();
cache_factory::reset();
cache_config_testing::create_default_configuration();
}
/**
* Final task is to reset the cache system
*/
public static function tearDownAfterClass(): void {
parent::tearDownAfterClass();
cache_factory::reset();
}
/**
* Test the numerous summaries the helper can produce.
*/
public function test_get_summaries(): void {
// First the preparation.
$config = cache_config_writer::instance();
$this->assertTrue($config->add_store_instance('summariesstore', 'file'));
$config->set_definition_mappings('core/eventinvalidation', array('summariesstore'));
$this->assertTrue($config->set_mode_mappings(array(
cache_store::MODE_APPLICATION => array('summariesstore'),
cache_store::MODE_SESSION => array('default_session'),
cache_store::MODE_REQUEST => array('default_request'),
)));
$storesummaries = administration_helper::get_store_instance_summaries();
$this->assertIsArray($storesummaries);
$this->assertArrayHasKey('summariesstore', $storesummaries);
$summary = $storesummaries['summariesstore'];
// Check the keys
$this->assertArrayHasKey('name', $summary);
$this->assertArrayHasKey('plugin', $summary);
$this->assertArrayHasKey('default', $summary);
$this->assertArrayHasKey('isready', $summary);
$this->assertArrayHasKey('requirementsmet', $summary);
$this->assertArrayHasKey('mappings', $summary);
$this->assertArrayHasKey('modes', $summary);
$this->assertArrayHasKey('supports', $summary);
// Check the important/known values
$this->assertEquals('summariesstore', $summary['name']);
$this->assertEquals('file', $summary['plugin']);
$this->assertEquals(0, $summary['default']);
$this->assertEquals(1, $summary['isready']);
$this->assertEquals(1, $summary['requirementsmet']);
// Find the number of mappings to sessionstore.
$mappingcount = count(array_filter($config->get_definitions(), function($element) {
return $element['mode'] === cache_store::MODE_APPLICATION;
}));
$this->assertEquals($mappingcount, $summary['mappings']);
$definitionsummaries = administration_helper::get_definition_summaries();
$this->assertIsArray($definitionsummaries);
$this->assertArrayHasKey('core/eventinvalidation', $definitionsummaries);
$summary = $definitionsummaries['core/eventinvalidation'];
// Check the keys
$this->assertArrayHasKey('id', $summary);
$this->assertArrayHasKey('name', $summary);
$this->assertArrayHasKey('mode', $summary);
$this->assertArrayHasKey('component', $summary);
$this->assertArrayHasKey('area', $summary);
$this->assertArrayHasKey('mappings', $summary);
// Check the important/known values
$this->assertEquals('core/eventinvalidation', $summary['id']);
$this->assertInstanceOf('lang_string', $summary['name']);
$this->assertEquals(cache_store::MODE_APPLICATION, $summary['mode']);
$this->assertEquals('core', $summary['component']);
$this->assertEquals('eventinvalidation', $summary['area']);
$this->assertIsArray($summary['mappings']);
$this->assertContains('summariesstore', $summary['mappings']);
$pluginsummaries = administration_helper::get_store_plugin_summaries();
$this->assertIsArray($pluginsummaries);
$this->assertArrayHasKey('file', $pluginsummaries);
$summary = $pluginsummaries['file'];
// Check the keys
$this->assertArrayHasKey('name', $summary);
$this->assertArrayHasKey('requirementsmet', $summary);
$this->assertArrayHasKey('instances', $summary);
$this->assertArrayHasKey('modes', $summary);
$this->assertArrayHasKey('supports', $summary);
$this->assertArrayHasKey('canaddinstance', $summary);
$locksummaries = administration_helper::get_lock_summaries();
$this->assertIsArray($locksummaries);
$this->assertTrue(count($locksummaries) > 0);
$mappings = administration_helper::get_default_mode_stores();
$this->assertIsArray($mappings);
$this->assertCount(3, $mappings);
$this->assertArrayHasKey(cache_store::MODE_APPLICATION, $mappings);
$this->assertIsArray($mappings[cache_store::MODE_APPLICATION]);
$this->assertContains('summariesstore', $mappings[cache_store::MODE_APPLICATION]);
$potentials = administration_helper::get_definition_store_options('core', 'eventinvalidation');
$this->assertIsArray($potentials); // Currently used, suitable, default
$this->assertCount(3, $potentials);
$this->assertArrayHasKey('summariesstore', $potentials[0]);
$this->assertArrayHasKey('summariesstore', $potentials[1]);
$this->assertArrayHasKey('default_application', $potentials[1]);
}
/**
* Test instantiating an add store form.
*/
public function test_get_add_store_form(): void {
$form = cache_factory::get_administration_display_helper()->get_add_store_form('file');
$this->assertInstanceOf('moodleform', $form);
try {
$form = cache_factory::get_administration_display_helper()->get_add_store_form('somethingstupid');
$this->fail('You should not be able to create an add form for a store plugin that does not exist.');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e, 'Needs to be: ' .get_class($e)." ::: ".$e->getMessage());
}
}
/**
* Test instantiating a form to edit a store instance.
*/
public function test_get_edit_store_form(): void {
// Always instantiate a new core display helper here.
$administrationhelper = new local\administration_display_helper;
$config = cache_config_writer::instance();
$this->assertTrue($config->add_store_instance('test_get_edit_store_form', 'file'));
$form = $administrationhelper->get_edit_store_form('file', 'test_get_edit_store_form');
$this->assertInstanceOf('moodleform', $form);
try {
$form = $administrationhelper->get_edit_store_form('somethingstupid', 'moron');
$this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
$form = $administrationhelper->get_edit_store_form('file', 'blisters');
$this->fail('You should not be able to create an edit form for a store plugin that does not exist.');
} catch (\moodle_exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
/**
* Test the hash_key functionality.
*/
public function test_hash_key(): void {
$this->resetAfterTest();
set_debugging(DEBUG_ALL);
// First with simplekeys
$instance = cache_config_testing::instance(true);
$instance->phpunit_add_definition('phpunit/hashtest', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'hashtest',
'simplekeys' => true
));
$factory = cache_factory::instance();
$definition = $factory->create_definition('phpunit', 'hashtest');
$result = cache_helper::hash_key('test', $definition);
$this->assertEquals('test-'.$definition->generate_single_key_prefix(), $result);
try {
cache_helper::hash_key('test/test', $definition);
$this->fail('Invalid key was allowed, you should see this.');
} catch (\coding_exception $e) {
$this->assertEquals('test/test', $e->debuginfo);
}
// Second without simple keys
$instance->phpunit_add_definition('phpunit/hashtest2', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'hashtest2',
'simplekeys' => false
));
$definition = $factory->create_definition('phpunit', 'hashtest2');
$result = cache_helper::hash_key('test', $definition);
$this->assertEquals(sha1($definition->generate_single_key_prefix().'-test'), $result);
$result = cache_helper::hash_key('test/test', $definition);
$this->assertEquals(sha1($definition->generate_single_key_prefix().'-test/test'), $result);
}
/**
* Tests the get_usage function.
*/
public function test_get_usage(): void {
// Create a test cache definition and put items in it.
$instance = cache_config_testing::instance(true);
$instance->phpunit_add_definition('phpunit/test', [
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'test',
'simplekeys' => true
]);
$cache = \cache::make('phpunit', 'test');
for ($i = 0; $i < 100; $i++) {
$cache->set('key' . $i, str_repeat('x', $i));
}
$factory = cache_factory::instance();
$adminhelper = $factory->get_administration_display_helper();
$usage = $adminhelper->get_usage(10)['phpunit/test'];
$this->assertEquals('phpunit/test', $usage->cacheid);
$this->assertCount(1, $usage->stores);
$store = $usage->stores[0];
$this->assertEquals('default_application', $store->name);
$this->assertEquals('cachestore_file', $store->class);
$this->assertEquals(true, $store->supported);
$this->assertEquals(100, $store->items);
// As file store checks all items, the values should be exact.
$this->assertEqualsWithDelta(57.4, $store->mean, 0.1);
$this->assertEqualsWithDelta(29.0, $store->sd, 0.1);
$this->assertEquals(0, $store->margin);
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
/**
* Unit tests for {@see allow_temporary_caches}.
*
* @package core_cache
* @category test
* @copyright 2022 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_cache\allow_temporary_caches
*/
class allow_temporary_caches_test extends \advanced_testcase {
/**
* Tests whether temporary caches are allowed.
*/
public function test_is_allowed(): void {
// Not allowed by default.
$this->assertFalse(allow_temporary_caches::is_allowed());
// Allowed if we have an instance.
$frog = new allow_temporary_caches();
$this->assertTrue(allow_temporary_caches::is_allowed());
// Or two instances.
$toad = new allow_temporary_caches();
$this->assertTrue(allow_temporary_caches::is_allowed());
// Get rid of the instances.
unset($frog);
$this->assertTrue(allow_temporary_caches::is_allowed());
// Not allowed when we get back to no instances.
unset($toad);
$this->assertFalse(allow_temporary_caches::is_allowed());
// Check it works to automatically free up the instance when variable goes out of scope.
$this->inner_is_allowed();
$this->assertFalse(allow_temporary_caches::is_allowed());
}
/**
* Function call to demonstrate that you don't need to manually unset the variable.
*/
protected function inner_is_allowed(): void {
$gecko = new allow_temporary_caches();
$this->assertTrue(allow_temporary_caches::is_allowed());
}
/**
* Tests that the temporary caches actually work, including normal and versioned get and set.
*/
public function test_temporary_cache(): void {
$this->resetAfterTest();
// Disable the cache.
\cache_phpunit_factory::phpunit_disable();
try {
// Try using the cache now - it returns false/null for everything.
$cache = \cache::make('core', 'coursemodinfo');
$cache->set('frog', 'ribbit');
$this->assertFalse($cache->get('frog'));
$cache->set_versioned('toad', 2, 'croak');
$this->assertFalse($cache->get_versioned('toad', 2));
// But when we allow temporary caches, it should work as normal.
$allow = new allow_temporary_caches();
$cache = \cache::make('core', 'coursemodinfo');
$cache->set('frog', 'ribbit');
$this->assertEquals('ribbit', $cache->get('frog'));
$cache->set_versioned('toad', 2, 'croak');
$this->assertEquals('croak', $cache->get_versioned('toad', 2));
// Let's actually use modinfo, to check it works with locking too.
$course = $this->getDataGenerator()->create_course();
get_fast_modinfo($course);
} finally {
// You have to do this after phpunit_disable or it breaks later tests.
\cache_factory::reset();
\cache_factory::instance(true);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
@core @core_cache
Feature: Display cache information in performance info
In order to investigate performance problems with caching
As an administrator
I need to be able to see cache information in perfinfo
Background:
Given the following config values are set as admin:
| perfdebug | 15 |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
Scenario: Cache performance info displays
When I am on the "C1" "Course" page logged in as "admin"
# Confirm that the first cache info table is visible by checking an arbitrary row.
Then I should see "default_application" in the "core/databasemeta" "table_row"
# Don't specify the exact size as it may vary.
And I should see "KB" in the "core/databasemeta" "table_row"
# Confirm that the second cache info table is visible.
And I should see "default_application" in the "cachestore_file" "table_row"
+27
View File
@@ -0,0 +1,27 @@
@core @core_cache @javascript
Feature: Verify the breadcrumbs in different cache site administration pages
Whenever I navigate to caching configuration page in site administration
As an admin
The breadcrumbs should be visible
Background:
Given I log in as "admin"
Scenario: Verify the breadcrumbs in caching configuration, add assistance, edit mappings and edit sharings page as an admin
Given I navigate to "Plugins > Caching > Configuration" in site administration
And "Configuration" "text" should exist in the ".breadcrumb" "css_element"
And "Caching" "link" should exist in the ".breadcrumb" "css_element"
When I click on "Add instance" "link"
Then "Add cache store" "text" should exist in the ".breadcrumb" "css_element"
And "Configuration" "link" should exist in the ".breadcrumb" "css_element"
And "Caching" "link" should exist in the ".breadcrumb" "css_element"
And I press "Cancel"
And I click on "Edit mappings" "link"
And "Edit definition mapping" "text" should exist in the ".breadcrumb" "css_element"
And "Configuration" "link" should exist in the ".breadcrumb" "css_element"
And "Caching" "link" should exist in the ".breadcrumb" "css_element"
And I press "Cancel"
And I click on "Edit sharing" "link"
And "Edit definition sharing" "text" should exist in the ".breadcrumb" "css_element"
And "Configuration" "link" should exist in the ".breadcrumb" "css_element"
And "Caching" "link" should exist in the ".breadcrumb" "css_element"
+35
View File
@@ -0,0 +1,35 @@
@core @core_cache
Feature: Display usage information for cache
In order to investigate performance problems with caching
As an administrator
I need to be able to monitor the size of items in the cache
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
Scenario: Cache performance info displays
When I am on the "C1" "Course" page logged in as "admin"
And I navigate to "Plugins > Caching > Cache usage" in site administration
# Check one row of the summary table. The actual total is currently 3.6MB so it's likely to
# continue to be in the MB range.
Then "default_application" row "Plugin" column of "usage_summary" table should contain "cachestore_file"
And "default_application" row "Estimated total" column of "usage_summary" table should contain "MB"
And "default_application" row "Actual usage (if known)" column of "usage_summary" table should contain "MB"
# And one row of the main table. The totals are fixed to use the MB unit.
And "core/config" row "Store name" column of "usage_main" table should contain "default_application"
And "core/config" row "Plugin" column of "usage_main" table should contain "cachestore_file"
And "core/config" row "Estimated total" column of "usage_main" table should contain "MB"
Scenario: Sample option works
When I am on the "C1" "Course" page logged in as "admin"
And I navigate to "Plugins > Caching > Cache usage" in site administration
And I set the field "samples" to "1000"
And I press "Update"
Then the field "samples" matches value "1000"
And "usage_summary" "table" should exist
And "usage_main" "table" should exist
+64
View File
@@ -0,0 +1,64 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
/**
* PHPunit tests for the cache_helper class.
*
* @package core
* @category cache
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \cache_helper
*/
class cache_helper_test extends \advanced_testcase {
/**
* Test the result_found method.
*
* @param mixed $value
* @param bool $expected
* @dataProvider result_found_provider
* @covers ::result_found
*/
public function test_result_found($value, bool $expected): void {
$this->assertEquals($expected, \cache_helper::result_found($value));
}
/**
* Data provider for result_found tests.
*
* @return array
*/
public function result_found_provider(): array {
return [
// Only false values are considered as not found.
[false, false],
// The rest are considered valid values.
[null, true],
[0, true],
['', true],
[[], true],
[new \stdClass(), true],
[true, true],
[1, true],
['a', true],
[[1], true],
[new \stdClass(), true],
];
}
}
+3288
View File
File diff suppressed because it is too large Load Diff
+287
View File
@@ -0,0 +1,287 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
use cache_config_testing;
use cache_config_writer;
use cache_factory;
use cache_store;
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 and in particular the cache config writer.
*
* @package core_cache
* @category test
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class config_writer_test extends \advanced_testcase {
/**
* Set things back to the default before each test.
*/
public function setUp(): void {
parent::setUp();
cache_factory::reset();
cache_config_testing::create_default_configuration();
}
/**
* Final task is to reset the cache system
*/
public static function tearDownAfterClass(): void {
parent::tearDownAfterClass();
cache_factory::reset();
}
/**
* Test getting an instance. Pretty basic.
*/
public function test_instance(): void {
$config = cache_config_writer::instance();
$this->assertInstanceOf('cache_config_writer', $config);
}
/**
* Test the default configuration.
*/
public function test_default_configuration(): void {
$config = cache_config_writer::instance();
// First check stores.
$stores = $config->get_all_stores();
$hasapplication = false;
$hassession = false;
$hasrequest = false;
foreach ($stores as $store) {
// Check the required keys.
$this->assertArrayHasKey('name', $store);
$this->assertArrayHasKey('plugin', $store);
$this->assertArrayHasKey('modes', $store);
$this->assertArrayHasKey('default', $store);
// Check the mode, we need at least one default store of each mode.
if (!empty($store['default'])) {
if ($store['modes'] & cache_store::MODE_APPLICATION) {
$hasapplication = true;
}
if ($store['modes'] & cache_store::MODE_SESSION) {
$hassession = true;
}
if ($store['modes'] & cache_store::MODE_REQUEST) {
$hasrequest = true;
}
}
}
$this->assertTrue($hasapplication, 'There is no default application cache store.');
$this->assertTrue($hassession, 'There is no default session cache store.');
$this->assertTrue($hasrequest, 'There is no default request cache store.');
// Next check the definitions.
$definitions = $config->get_definitions();
$eventinvalidation = false;
foreach ($definitions as $definition) {
// Check the required keys.
$this->assertArrayHasKey('mode', $definition);
$this->assertArrayHasKey('component', $definition);
$this->assertArrayHasKey('area', $definition);
if ($definition['component'] === 'core' && $definition['area'] === 'eventinvalidation') {
$eventinvalidation = true;
}
}
$this->assertTrue($eventinvalidation, 'Missing the event invalidation definition.');
// Next mode mappings
$mappings = $config->get_mode_mappings();
$hasapplication = false;
$hassession = false;
$hasrequest = false;
foreach ($mappings as $mode) {
// Check the required keys.
$this->assertArrayHasKey('mode', $mode);
$this->assertArrayHasKey('store', $mode);
if ($mode['mode'] === cache_store::MODE_APPLICATION) {
$hasapplication = true;
}
if ($mode['mode'] === cache_store::MODE_SESSION) {
$hassession = true;
}
if ($mode['mode'] === cache_store::MODE_REQUEST) {
$hasrequest = true;
}
}
$this->assertTrue($hasapplication, 'There is no mapping for the application mode.');
$this->assertTrue($hassession, 'There is no mapping for the session mode.');
$this->assertTrue($hasrequest, 'There is no mapping for the request mode.');
// Finally check config locks
$locks = $config->get_locks();
foreach ($locks as $lock) {
$this->assertArrayHasKey('name', $lock);
$this->assertArrayHasKey('type', $lock);
$this->assertArrayHasKey('default', $lock);
}
// There has to be at least the default lock.
$this->assertTrue(count($locks) > 0);
}
/**
* Test updating the definitions.
*/
public function test_update_definitions(): void {
$config = cache_config_writer::instance();
// Remove the definition.
$config->phpunit_remove_definition('core/string');
$definitions = $config->get_definitions();
// Check it is gone.
$this->assertFalse(array_key_exists('core/string', $definitions));
// Update definitions. This should re-add it.
cache_config_writer::update_definitions();
$definitions = $config->get_definitions();
// Check it is back again.
$this->assertTrue(array_key_exists('core/string', $definitions));
}
/**
* Test adding/editing/deleting store instances.
*/
public function test_add_edit_delete_plugin_instance(): void {
$config = cache_config_writer::instance();
$this->assertArrayNotHasKey('addplugintest', $config->get_all_stores());
$this->assertArrayNotHasKey('addplugintestwlock', $config->get_all_stores());
// Add a default file instance.
$config->add_store_instance('addplugintest', 'file');
cache_factory::reset();
$config = cache_config_writer::instance();
$this->assertArrayHasKey('addplugintest', $config->get_all_stores());
// Add a store with a lock described.
$config->add_store_instance('addplugintestwlock', 'file', array('lock' => 'default_file_lock'));
$this->assertArrayHasKey('addplugintestwlock', $config->get_all_stores());
$config->delete_store_instance('addplugintest');
$this->assertArrayNotHasKey('addplugintest', $config->get_all_stores());
$this->assertArrayHasKey('addplugintestwlock', $config->get_all_stores());
$config->delete_store_instance('addplugintestwlock');
$this->assertArrayNotHasKey('addplugintest', $config->get_all_stores());
$this->assertArrayNotHasKey('addplugintestwlock', $config->get_all_stores());
// Add a default file instance.
$config->add_store_instance('storeconfigtest', 'file', array('test' => 'a', 'one' => 'two'));
$stores = $config->get_all_stores();
$this->assertArrayHasKey('storeconfigtest', $stores);
$this->assertArrayHasKey('configuration', $stores['storeconfigtest']);
$this->assertArrayHasKey('test', $stores['storeconfigtest']['configuration']);
$this->assertArrayHasKey('one', $stores['storeconfigtest']['configuration']);
$this->assertEquals('a', $stores['storeconfigtest']['configuration']['test']);
$this->assertEquals('two', $stores['storeconfigtest']['configuration']['one']);
$config->edit_store_instance('storeconfigtest', 'file', array('test' => 'b', 'one' => 'three'));
$stores = $config->get_all_stores();
$this->assertArrayHasKey('storeconfigtest', $stores);
$this->assertArrayHasKey('configuration', $stores['storeconfigtest']);
$this->assertArrayHasKey('test', $stores['storeconfigtest']['configuration']);
$this->assertArrayHasKey('one', $stores['storeconfigtest']['configuration']);
$this->assertEquals('b', $stores['storeconfigtest']['configuration']['test']);
$this->assertEquals('three', $stores['storeconfigtest']['configuration']['one']);
$config->delete_store_instance('storeconfigtest');
try {
$config->delete_store_instance('default_application');
$this->fail('Default store deleted. This should not be possible!');
} catch (\Exception $e) {
$this->assertInstanceOf('cache_exception', $e);
}
try {
$config->delete_store_instance('some_crazy_store');
$this->fail('You should not be able to delete a store that does not exist.');
} catch (\Exception $e) {
$this->assertInstanceOf('cache_exception', $e);
}
try {
// Try with a plugin that does not exist.
$config->add_store_instance('storeconfigtest', 'shallowfail', array('test' => 'a', 'one' => 'two'));
$this->fail('You should not be able to add an instance of a store that does not exist.');
} catch (\Exception $e) {
$this->assertInstanceOf('cache_exception', $e);
}
}
/**
* Test setting some mode mappings.
*/
public function test_set_mode_mappings(): void {
$config = cache_config_writer::instance();
$this->assertTrue($config->add_store_instance('setmodetest', 'file'));
$this->assertTrue($config->set_mode_mappings(array(
cache_store::MODE_APPLICATION => array('setmodetest', 'default_application'),
cache_store::MODE_SESSION => array('default_session'),
cache_store::MODE_REQUEST => array('default_request'),
)));
$mappings = $config->get_mode_mappings();
$setmodetestfound = false;
foreach ($mappings as $mapping) {
if ($mapping['store'] == 'setmodetest' && $mapping['mode'] == cache_store::MODE_APPLICATION) {
$setmodetestfound = true;
}
}
$this->assertTrue($setmodetestfound, 'Set mapping did not work as expected.');
}
/**
* Test setting some definition mappings.
*/
public function test_set_definition_mappings(): void {
$config = cache_config_testing::instance(true);
$config->phpunit_add_definition('phpunit/testdefinition', array(
'mode' => cache_store::MODE_APPLICATION,
'component' => 'phpunit',
'area' => 'testdefinition'
));
$config = cache_config_writer::instance();
$this->assertTrue($config->add_store_instance('setdefinitiontest', 'file'));
$this->assertIsArray($config->get_definition_by_id('phpunit/testdefinition'));
$config->set_definition_mappings('phpunit/testdefinition', array('setdefinitiontest', 'default_application'));
try {
$config->set_definition_mappings('phpunit/testdefinition', array('something that does not exist'));
$this->fail('You should not be able to set a mapping for a store that does not exist.');
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
try {
$config->set_definition_mappings('something/crazy', array('setdefinitiontest'));
$this->fail('You should not be able to set a mapping for a definition that does not exist.');
} catch (\Exception $e) {
$this->assertInstanceOf('coding_exception', $e);
}
}
}
@@ -0,0 +1,84 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A dummy datasource which supports versioning.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_dummy_datasource_versionable extends cache_phpunit_dummy_datasource
implements cache_data_source_versionable {
/** @var array Data in cache */
protected $data = [];
/** @var cache_phpunit_dummy_datasource_versionable Last created instance */
protected static $lastinstance;
/**
* Returns an instance of this object for use with the cache.
*
* @param cache_definition $definition
* @return cache_phpunit_dummy_datasource New object
*/
public static function get_instance_for_cache(cache_definition $definition):
cache_phpunit_dummy_datasource_versionable {
self::$lastinstance = new cache_phpunit_dummy_datasource_versionable();
return self::$lastinstance;
}
/**
* Gets the last instance that was created.
*
* @return cache_phpunit_dummy_datasource_versionable
*/
public static function get_last_instance(): cache_phpunit_dummy_datasource_versionable {
return self::$lastinstance;
}
/**
* Sets up the datasource so that it has a value for a particular key.
*
* @param string $key Key
* @param int $version Version for key
* @param mixed $data
*/
public function has_value(string $key, int $version, $data): void {
$this->data[$key] = new \core_cache\version_wrapper($data, $version);
}
/**
* Loads versioned data.
*
* @param int|string $key Key
* @param int $requiredversion Minimum version number
* @param mixed $actualversion Should be set to the actual version number retrieved
* @return mixed Data retrieved from cache or false if none
*/
public function load_for_cache_versioned($key, int $requiredversion, &$actualversion) {
if (!array_key_exists($key, $this->data)) {
return false;
}
$value = $this->data[$key];
if ($value->version < $requiredversion) {
return false;
}
$actualversion = $value->version;
return $value->data;
}
}
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A subclass of cachestore_file but which doesn't report that it has TTL support.
*
* This is so we can easily test behaviour involving the TTL wrapper objects.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_file_with_ttl_wrappers extends cachestore_file {
/**
* Reports the same supported features as the parent, but without SUPPORTS_NATIVE_TTL.
*
* @param array $configuration Configuration
* @return int Supported features
*/
public static function get_supported_features(array $configuration = array()) {
return parent::get_supported_features($configuration) - self::SUPPORTS_NATIVE_TTL;
}
}
+600
View File
@@ -0,0 +1,600 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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();
require_once($CFG->dirroot.'/cache/locallib.php');
/**
* Override the default cache configuration for our own maniacal purposes.
*
* This class was originally named cache_config_phpunittest but was renamed in 2.9
* because it is used for both unit tests and acceptance tests.
*
* @since 2.9
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_config_testing extends cache_config_writer {
/**
* 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.
*
* @param bool $forcesave If set to true then we will forcefully save the default configuration file.
* @return true|array Returns true if the default configuration was successfully created.
* Returns a configuration array if it could not be saved. This is a bad situation. Check your error logs.
*/
public static function create_default_configuration($forcesave = false) {
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.
$writer = new self;
$writer->configstores = self::get_default_stores();
$writer->configdefinitions = self::locate_definitions();
$defaultapplication = 'default_application';
$appdefine = defined('TEST_CACHE_USING_APPLICATION_STORE') ? TEST_CACHE_USING_APPLICATION_STORE : false;
if ($appdefine !== false && preg_match('/^[a-zA-Z][a-zA-Z0-9_]+$/', $appdefine)) {
$expectedstore = $appdefine;
$file = $CFG->dirroot.'/cache/stores/'.$appdefine.'/lib.php';
$class = 'cachestore_'.$appdefine;
if (file_exists($file)) {
require_once($file);
}
if (class_exists($class) && $class::ready_to_be_used_for_testing()) {
/* @var cache_store $class */
$writer->configstores['test_application'] = array(
'name' => 'test_application',
'plugin' => $expectedstore,
'modes' => $class::get_supported_modes(),
'features' => $class::get_supported_features(),
'configuration' => $class::unit_test_configuration()
);
$defaultapplication = 'test_application';
}
}
$writer->configmodemappings = array(
array(
'mode' => cache_store::MODE_APPLICATION,
'store' => $defaultapplication,
'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->configlocks = array(
'default_file_lock' => array(
'name' => 'cachelock_file_default',
'type' => 'cachelock_file',
'dir' => 'filelocks',
'default' => true
)
);
$factory = cache_factory::instance();
// We expect the cache to be initialising presently. If its not then something has gone wrong and likely
// we are now in a loop.
if (!$forcesave && $factory->get_state() !== cache_factory::STATE_INITIALISING) {
return $writer->generate_configuration_array();
}
$factory->set_state(cache_factory::STATE_SAVING);
$writer->config_save();
return true;
}
/**
* Returns the expected path to the configuration file.
*
* We override this function to add handling for $CFG->altcacheconfigpath.
* We want to support it so that people can run unit tests against alternative cache setups.
* However we don't want to ever make changes to the file at $CFG->altcacheconfigpath so we
* always use dataroot and copy the alt file there as required.
*
* @throws cache_exception
* @return string The absolute path
*/
protected static function get_config_file_path() {
global $CFG;
// We always use this path.
$configpath = $CFG->dataroot.'/muc/config.php';
if (!empty($CFG->altcacheconfigpath)) {
// No need to check we are within a test here, this is the cache config class that gets used
// only when one of those is true.
if (!defined('TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH') || !TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH) {
// TEST_CACHE_USING_ALT_CACHE_CONFIG_PATH has not being defined or is false, we want to use the default.
return $configpath;
}
$path = $CFG->altcacheconfigpath;
if (is_dir($path) && is_writable($path)) {
// Its a writable directory, thats fine. Convert it to a file.
$path = $CFG->altcacheconfigpath.'/cacheconfig.php';
}
if (is_readable($path)) {
$directory = dirname($configpath);
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. Check the permissions on your moodledata directory.');
}
}
// We don't care that this fails but we should let the developer know.
if (!is_readable($configpath) && !@copy($path, $configpath)) {
debugging('Failed to copy alt cache config file to required location');
}
}
}
// We always use the dataroot location.
return $configpath;
}
/**
* Adds a definition to the stack
* @param string $area
* @param array $properties
* @param bool $addmapping By default this method adds a definition and a mapping for that definition. You can
* however set this to false if you only want it to add the definition and not the mapping.
*/
public function phpunit_add_definition($area, array $properties, $addmapping = true) {
if (!array_key_exists('overrideclass', $properties)) {
switch ($properties['mode']) {
case cache_store::MODE_APPLICATION:
$properties['overrideclass'] = 'cache_phpunit_application';
break;
case cache_store::MODE_SESSION:
$properties['overrideclass'] = 'cache_phpunit_session';
break;
case cache_store::MODE_REQUEST:
$properties['overrideclass'] = 'cache_phpunit_request';
break;
}
}
$this->configdefinitions[$area] = $properties;
if ($addmapping) {
switch ($properties['mode']) {
case cache_store::MODE_APPLICATION:
$this->phpunit_add_definition_mapping($area, 'default_application', 0);
break;
case cache_store::MODE_SESSION:
$this->phpunit_add_definition_mapping($area, 'default_session', 0);
break;
case cache_store::MODE_REQUEST:
$this->phpunit_add_definition_mapping($area, 'default_request', 0);
break;
}
}
}
/**
* Removes a definition.
* @param string $name
*/
public function phpunit_remove_definition($name) {
unset($this->configdefinitions[$name]);
}
/**
* Removes the configured stores so that there are none available.
*/
public function phpunit_remove_stores() {
$this->configstores = array();
}
/**
* Forcefully adds a file store.
*
* You can turn off native TTL support if you want a way to test TTL wrapper objects.
*
* @param string $name
* @param bool $nativettl If false, uses fixture that turns off native TTL support
*/
public function phpunit_add_file_store(string $name, bool $nativettl = true): void {
if (!$nativettl) {
require_once(__DIR__ . '/cachestore_file_with_ttl_wrappers.php');
}
$this->configstores[$name] = array(
'name' => $name,
'plugin' => 'file',
'configuration' => array(
'path' => ''
),
'features' => 6,
'modes' => 3,
'mappingsonly' => false,
'class' => $nativettl ? 'cachestore_file' : 'cachestore_file_with_ttl_wrappers',
'default' => false,
'lock' => 'cachelock_file_default'
);
}
/**
* Hacks the in-memory configuration for a store.
*
* @param string $store Name of store to edit e.g. 'default_application'
* @param array $configchanges List of config changes
*/
public function phpunit_edit_store_config(string $store, array $configchanges): void {
foreach ($configchanges as $name => $value) {
$this->configstores[$store]['configuration'][$name] = $value;
}
}
/**
* Forcefully adds a session store.
*
* @param string $name
*/
public function phpunit_add_session_store($name) {
$this->configstores[$name] = array(
'name' => $name,
'plugin' => 'session',
'configuration' => array(),
'features' => 14,
'modes' => 2,
'default' => true,
'class' => 'cachestore_session',
'lock' => 'cachelock_file_default',
);
}
/**
* Forcefully injects a definition => store mapping.
*
* This function does no validation, you should only be calling if it you know
* exactly what to expect.
*
* @param string $definition
* @param string $store
* @param int $sort
*/
public function phpunit_add_definition_mapping($definition, $store, $sort) {
$this->configdefinitionmappings[] = array(
'store' => $store,
'definition' => $definition,
'sort' => (int)$sort
);
}
/**
* Overrides the default site identifier used by the Cache API so that we can be sure of what it is.
*
* @return string
*/
public function get_site_identifier() {
global $CFG;
return $CFG->wwwroot.'phpunit';
}
/**
* Checks if the configuration file exists.
*
* @return bool True if it exists
*/
public static function config_file_exists() {
// Allow for late static binding by using static.
$configfilepath = static::get_config_file_path();
// Invalidate opcode php cache, so we get correct status of file.
core_component::invalidate_opcode_php_cache($configfilepath);
return file_exists($configfilepath);
}
}
/**
* Dummy object for testing cacheable object interface and interaction
*
* Wake from cache needs specific testing at times to ensure that during multiple
* cache get() requests it's possible to verify that it's getting woken each time.
*
* @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;
/**
* Test property time for verifying wake is run at each get() call.
* @var float
*/
public $propertytime;
/**
* Constructor
* @param string $property1
* @param string $property2
*/
public function __construct($property1, $property2, $propertytime = null) {
$this->property1 = $property1;
$this->property2 = $property2;
$this->propertytime = $propertytime === null ? microtime(true) : $propertytime;
}
/**
* Prepares this object for caching
* @return array
*/
public function prepare_to_cache() {
return array($this->property1.'_ptc', $this->property2.'_ptc', $this->propertytime);
}
/**
* Returns this object from the cache
* @param array $data
* @return cache_phpunit_dummy_object
*/
public static function wake_from_cache($data) {
$time = null;
if (!is_null($data[2])) {
// Windows 32bit microtime() resolution is 15ms, we ensure the time has moved forward.
do {
$time = microtime(true);
} while ($time == $data[2]);
}
return new cache_phpunit_dummy_object(array_shift($data).'_wfc', array_shift($data).'_wfc', $time);
}
}
/**
* 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;
}
}
/**
* PHPUnit application cache loader.
*
* Used to expose things we could not otherwise see within an application cache.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_application extends cache_application {
/**
* Returns the class of the store immediately associated with this cache.
* @return string
*/
public function phpunit_get_store_class() {
return get_class($this->get_store());
}
/**
* Returns all the interfaces the cache store implements.
* @return array
*/
public function phpunit_get_store_implements() {
return class_implements($this->get_store());
}
/**
* Returns the given key directly from the static acceleration array.
*
* @param string $key
* @return false|mixed
*/
public function phpunit_static_acceleration_get($key) {
return $this->static_acceleration_get($key);
}
/**
* Purges only the static acceleration while leaving the rest of the store in tack.
*
* Used for behaving like you have loaded 2 pages, and reset static while the backing store
* still contains all the same data.
*
*/
public function phpunit_static_acceleration_purge() {
$this->static_acceleration_purge();
}
}
/**
* PHPUnit session cache loader.
*
* Used to expose things we could not otherwise see within an session cache.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_session extends cache_session {
/** @var Static member used for emulating the behaviour of session_id() during the tests. */
protected static $sessionidmockup = 'phpunitmockupsessionid';
/**
* Returns the class of the store immediately associated with this cache.
* @return string
*/
public function phpunit_get_store_class() {
return get_class($this->get_store());
}
/**
* Returns all the interfaces the cache store implements.
* @return array
*/
public function phpunit_get_store_implements() {
return class_implements($this->get_store());
}
/**
* Provide access to the {@link cache_session::get_key_prefix()} method.
*
* @return string
*/
public function phpunit_get_key_prefix() {
return $this->get_key_prefix();
}
/**
* Allows to inject the session identifier.
*
* @param string $sessionid
*/
public static function phpunit_mockup_session_id($sessionid) {
static::$sessionidmockup = $sessionid;
}
/**
* Override the parent behaviour so that it does not need the actual session_id() call.
*/
protected function set_session_id() {
$this->sessionid = static::$sessionidmockup;
}
}
/**
* PHPUnit request cache loader.
*
* Used to expose things we could not otherwise see within an request cache.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_request extends cache_request {
/**
* Returns the class of the store immediately associated with this cache.
* @return string
*/
public function phpunit_get_store_class() {
return get_class($this->get_store());
}
/**
* Returns all the interfaces the cache store implements.
* @return array
*/
public function phpunit_get_store_implements() {
return class_implements($this->get_store());
}
}
/**
* 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 {
// Satisfying the code pre-checker is just part of my day job.
}
/**
* Cache PHPUnit specific factory.
*
* @copyright 2012 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_factory extends cache_factory {
/**
* Exposes the cache_factory's disable method.
*
* Perhaps one day that method will be made public, for the time being it is protected.
*/
public static function phpunit_disable() {
parent::disable();
}
}
/**
* Cache PHPUnit specific Cache helper.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cache_phpunit_cache extends cache {
/**
* Make the changes which simulate a new request within the cache.
* This essentially resets currently held static values in the class, and increments the current timestamp.
*/
public static function simulate_new_request() {
self::$now += 0.1;
self::$purgetoken = null;
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Cache store test fixtures.
*
* @package core
* @category cache
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* An abstract class to make writing unit tests for cache stores very easy.
*
* @package core
* @category cache
* @copyright 2013 Sam Hemelryk
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class cachestore_tests extends advanced_testcase {
/**
* Returns the class name for the store.
*
* @return string
*/
abstract protected function get_class_name();
/**
* Sets up the fixture, for example, open a network connection.
* This method is called before a test is executed.
*/
public function setUp(): void {
$class = $this->get_class_name();
if (!class_exists($class) || !$class::are_requirements_met()) {
$this->markTestSkipped('Could not test '.$class.'. Requirements are not met.');
}
parent::setUp();
}
/**
* Run the unit tests for the store.
*/
public function test_test_instance() {
$class = $this->get_class_name();
$modes = $class::get_supported_modes();
if ($modes & cache_store::MODE_APPLICATION) {
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, $class, 'phpunit_test');
$instance = new $class($class.'_test', $class::unit_test_configuration());
if (!$instance->is_ready()) {
$this->markTestSkipped('Could not test '.$class.'. No test instance configured for application caches.');
} else {
$instance->initialise($definition);
$this->run_tests($instance);
}
}
if ($modes & cache_store::MODE_SESSION) {
$definition = cache_definition::load_adhoc(cache_store::MODE_SESSION, $class, 'phpunit_test');
$instance = new $class($class.'_test', $class::unit_test_configuration());
if (!$instance->is_ready()) {
$this->markTestSkipped('Could not test '.$class.'. No test instance configured for session caches.');
} else {
$instance->initialise($definition);
$this->run_tests($instance);
}
}
if ($modes & cache_store::MODE_REQUEST) {
$definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, $class, 'phpunit_test');
$instance = new $class($class.'_test', $class::unit_test_configuration());
if (!$instance->is_ready()) {
$this->markTestSkipped('Could not test '.$class.'. No test instance configured for request caches.');
} else {
$instance->initialise($definition);
$this->run_tests($instance);
}
}
}
/**
* Test the store for basic functionality.
*/
public function run_tests(cache_store $instance) {
$object = new stdClass;
$object->data = 1;
// Test set with a string.
$this->assertTrue($instance->set('test1', 'test1'));
$this->assertTrue($instance->set('test2', 'test2'));
$this->assertTrue($instance->set('test3', '3'));
$this->assertTrue($instance->set('other3', '3'));
// Test get with a string.
$this->assertSame('test1', $instance->get('test1'));
$this->assertSame('test2', $instance->get('test2'));
$this->assertSame('3', $instance->get('test3'));
// Test find and find with prefix if this class implements the searchable interface.
if ($instance->is_searchable()) {
// Extra settings here ignore the return order of the array.
$this->assertEqualsCanonicalizing(['test3', 'test1', 'test2', 'other3'], $instance->find_all());
// Extra settings here ignore the return order of the array.
$this->assertEqualsCanonicalizing(['test2', 'test1', 'test3'], $instance->find_by_prefix('test'));
$this->assertEquals(['test2'], $instance->find_by_prefix('test2'));
$this->assertEquals(['other3'], $instance->find_by_prefix('other'));
$this->assertEquals([], $instance->find_by_prefix('nothere'));
}
// Test set with an int.
$this->assertTrue($instance->set('test1', 1));
$this->assertTrue($instance->set('test2', 2));
// Test get with an int.
$this->assertSame(1, $instance->get('test1'));
$this->assertIsInt($instance->get('test1'));
$this->assertSame(2, $instance->get('test2'));
$this->assertIsInt($instance->get('test2'));
// Test set with a bool.
$this->assertTrue($instance->set('test1', true));
// Test get with an bool.
$this->assertSame(true, $instance->get('test1'));
$this->assertIsBool($instance->get('test1'));
// Test with an object.
$this->assertTrue($instance->set('obj', $object));
if ($instance::get_supported_features() & cache_store::DEREFERENCES_OBJECTS) {
$this->assertNotSame($object, $instance->get('obj'), 'Objects must be dereferenced when returned.');
}
$this->assertEquals($object, $instance->get('obj'));
// Test delete.
$this->assertTrue($instance->delete('test1'));
$this->assertTrue($instance->delete('test3'));
$this->assertFalse($instance->delete('test3'));
$this->assertFalse($instance->get('test1'));
$this->assertSame(2, $instance->get('test2'));
$this->assertTrue($instance->set('test1', 'test1'));
// Test purge.
$this->assertTrue($instance->purge());
$this->assertFalse($instance->get('test1'));
$this->assertFalse($instance->get('test2'));
// Test set_many.
$outcome = $instance->set_many(array(
array('key' => 'many1', 'value' => 'many1'),
array('key' => 'many2', 'value' => 'many2'),
array('key' => 'many3', 'value' => 'many3'),
array('key' => 'many4', 'value' => 'many4'),
array('key' => 'many5', 'value' => 'many5')
));
$this->assertSame(5, $outcome);
$this->assertSame('many1', $instance->get('many1'));
$this->assertSame('many5', $instance->get('many5'));
$this->assertFalse($instance->get('many6'));
// Test get_many.
$result = $instance->get_many(array('many1', 'many3', 'many5', 'many6'));
$this->assertIsArray($result);
$this->assertCount(4, $result);
$this->assertSame(array(
'many1' => 'many1',
'many3' => 'many3',
'many5' => 'many5',
'many6' => false,
), $result);
// Test delete_many.
$this->assertSame(3, $instance->delete_many(array('many2', 'many3', 'many4')));
$this->assertSame(2, $instance->delete_many(array('many1', 'many5', 'many6')));
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_cache;
/**
* Unit tests for cache_store functionality.
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class store_test extends \advanced_testcase {
/**
* Tests the default implementation of cache_size_details, which does some
* complicated statistics.
*/
public function test_cache_size_details(): void {
// Fill a store with 100 entries of varying size.
$store = self::create_static_store();
for ($i = 0; $i < 100; $i++) {
$store->set('key' . $i, str_repeat('x', $i));
}
// Do the statistics for 10 random picks.
$details = $store->cache_size_details(10);
$this->assertTrue($details->supported);
$this->assertEquals(100, $details->items);
// Min/max possible means if it picks the smallest/largest 10.
$this->assertGreaterThan(22, $details->mean);
$this->assertLessThan(115, $details->mean);
// Min/max possible SD.
$this->assertLessThan(49, $details->sd);
$this->assertGreaterThan(2.8, $details->sd);
// Lowest possible confidence margin is about 1.74.
$this->assertGreaterThan(1.7, $details->margin);
// Repeat the statistics for a pick of all 100 entries (exact).
$details = $store->cache_size_details(100);
$this->assertTrue($details->supported);
$this->assertEquals(100, $details->items);
$this->assertEqualsWithDelta(69.3, $details->mean, 0.1);
$this->assertEqualsWithDelta(29.2, $details->sd, 0.1);
$this->assertEquals(0, $details->margin);
}
/**
* Creates a static store for testing.
*
* @return \cachestore_static Store
*/
protected static function create_static_store(): \cachestore_static {
require_once(__DIR__ . '/../stores/static/lib.php');
$store = new \cachestore_static('frog');
$definition = \cache_definition::load('zombie', [
'mode' => \cache_store::MODE_REQUEST,
'component' => 'phpunit',
'area' => 'store_test',
'simplekeys' => true
]);
$store->initialise($definition);
return $store;
}
}
+129
View File
@@ -0,0 +1,129 @@
This files describes API changes in /cache/stores/* - cache store plugins.
Information provided here is intended especially for developers.
=== 4.3 ===
* A new cache_helper::result_found() helper has been added to assist with cache value validation
* Implementations of the following methods, deprecated since 3.10, have been removed and can no longer be used:
- `\core\lock\lock::extend`
- `\core\lock\lock_factory::extend_lock`
- `\core\lock\lock_factory::supports_recursion`
* The automatic locking options 'requirelockingread' and 'requirelockingwrite' have been removed.
These features are not used in core, were recommended against in the documentation, had significant
bugs, and had no useful purpose. 'requirelockingbeforewrite' is still available to check that manual
locking has been applied.
* The acquire_lock function within cache loaders now throws an exception if it fails to obtain the
lock after a timeout, instead of returning false.
* The functions set_many, delete, and delete_many now all throw exceptions if you are using
'requirelockingbeforewrite' and do not have a lock, same as the set function.
=== 4.2 ===
* The memcached cachestore has been removed.
* The mongodb cachestore has been removed.
=== 4.1 ===
* Added new `requirelockingbeforewrite` option for cache definitions. This will check that a lock for a given cache key already
exists before it will perform a `set()` on that key. A `coding_exception` is thrown if the lock has not been acquired.
* Added native locking to cachestore_file. This will use an instance of file_lock_factory pointing at a subdirectory in the same
location as the cache instance, meaning a local file cache will have its locks stored locally. If file locks are disabled
globally, it will fall back to use the default lock factory, which may not be in the same location as the cache. cachestore_file
includes an additional setting to control how long it will wait for a lock before giving up, default is 60 seconds.
=== 4.0 ===
* Cache stores may implement new optional function cache_store::get_last_io_bytes() to provide
information about the size of data transferred (shown in footer if performance info enabled).
* The cache_store class now has functions cache_size_details(), store_total_size(), and
estimate_stored_size(), related to size used by the cache. These can be overridden by a cache
store to provide better information for the new cache usage admin page.
* New functions cache::set_versioned() and cache::get_versioned() can be used to ensure correct
behaviour when using a multi-level cache with early cache levels stored locally. (Used when
rebuilding modinfo.) There is also a new interface cache_data_source_versionable which can
be implemented if you want to make a data source that supports versioning.
=== 3.10 ===
* The function supports_recursion() from the lock_factory interface has been deprecated including the related implementations.
* The function extend_lock() from the lock_factory interface has been deprecated without replacement including the related
implementations.
* The function extend() from the lock class has been deprecated without replacement.
* The cache_factory class can now be overridden by an alternative cache config class, which can
also now control the frontend display of the cache/admin.php page (see MDL-41492).
=== 3.9 ===
* The record_cache_hit/miss/set methods now take a cache_store instead of a cache_definition object
=== 3.8 ===
* The Redis cache store can now make use of the Zstandard compression algorithm (see MDL-66428).
=== 3.7 ===
* Upgraded MongoDB cache store to use the new lower level PHP-driver and MongoDB PHP Library.
* The mongodb extension has replaced the old mongo extension. The mongodb pecl extension >= 1.5 must be installed to use MongoDB
cache store.
=== 3.6 ===
* The `cache::now()` function now takes an optional boolean parameter to indicate that the cache should return a more
accurate time, generated by the PHP `microtime` function.
* The memcache store was removed as it is not compatible with PHP 7.0.
=== 3.3 ===
* Identifiers and invalidation events have been explictly been marked as incompatible and will
throw a coding exception. Unexpected results would have occurred if the previous behaviour was attempted.
* Identifiers are now part of loaded caches, so identifiers can only be set at cache::make()
a coding_exception will be thrown if attempts are made at other times.
Multiple calls to cache::make with different identifiers will produce 2 caches instead of changing the
keyspace of a single cache.
=== 3.2 ===
* The following methods have been finally deprecated and should no longer be used.
- cache_definition::should_be_persistent()
- cache_definition::get_persistent_max_size()
- cache::is_using_persist_cache()
- cache::is_in_persist_cache()
- cache::get_from_persist_cache()
- cache::set_in_persist_cache()
- cache::delete_from_persist_cache()
- cache_store::cleanup()
* cachestore_dummy::cleanup() has been deprecated.
* cachestore_dummy::instance_deleted() implemented in lieu of cachestore_dummy::cleanup().
* Added cache_store::unit_test_configuration() to calculate unit testing configuration.
* Remove cache_store:initialise_unit_test_instance() as it is incompatible with cache_helper purge functions.
=== 3.1 ===
* Cache stores has a new feature DEREFERENCES_OBJECTS.
This allows the cache loader to decide if it needs to handle dereferencing or whether the data
coming directly to it has already had references resolved.
- see supports_dereferencing_objects in store.php.
=== 2.9 ===
* Cache data source aggregation functionality has been removed. This functionality was found to be broken and unused.
It was decided that rather than fixing it it should be removed.
As well as the processing code being removed the following API changes have been made.
The following changes have come about because of it:
- cache_definition::$datasourceaggregate is deprecated an unused.
- cache_definition::load Argument 3 (final arg) is now unused.
- cache_factory::create_cache_from_definition Argument 4 (final arg) is now unused.
- cache::make Argument 4 (final arg) is now unused.
* cache_config_phpunittest has been renamed to cache_config_testing
* New method cache_store::ready_to_be_used_for_testing() that returns true|false if the store is suitable and ready for use as the primary store during unit and acceptance tests.
* cache_helper::get_stats structure we changed to include the cache mode.
=== 2.7 ===
* cache_store::is_ready is no longer abstract, calling cache_store::are_requirements_met by default.
=== 2.6 ===
* All cache instances are recorded and subsequent requests are given a reference to the original instance.
* The persistent option for the cache definition has been deprecated. Please use the staticacceleration option instead.
* There is a new static acceleration option. If enabled data passing through the cache is held onto.
* The persistentmaxsize option has been renamed to staticaccelerationsize. It does the same thing.
* cache_definition::should_be_persistent has been deprecated. Please call cache_definition::use_static_acceleration instead.
* cache_definition::get_persistent_max_size has been deprecated. Please call cache_definition::get_static_acceleration_size instead.
* cache::is_using_persist_cache() has been deprecated. Please call cache::use_static_acceleration()
* cache::is_in_persist_cache() has been deprecated. Please call cache::static_acceleration_has()
* cache::get_from_persist_cache() has been deprecated. Please call cache::static_acceleration_get()
* cache::set_in_persist_cache() has been deprecated. Please call cache::static_acceleration_set()
* cache::delete_from_persist_cache() has been deprecated. Please call cache::static_acceleration_delete()
* If you have any custom cache loaders you will need to rename these methods if you have overriden them and adjust any calls you may have made to them.
=== 2.5 ===
* cleanup method renamed to instance_deleted.
It is now called when the store is deleted as all comments suggested anyway.
* instance_created method added.
It is called when the store is created for the very first time.
+50
View File
@@ -0,0 +1,50 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Show current cache usage (number of items, size of caches).
*
* @package core_cache
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../config.php');
require_once($CFG->dirroot . '/lib/adminlib.php');
admin_externalpage_setup('cacheusage');
$adminhelper = cache_factory::instance()->get_administration_display_helper();
raise_memory_limit(MEMORY_EXTRA);
$samples = optional_param('samples', 50, PARAM_INT);
// Just for safety reasons, stop people choosing a stupid number.
if ($samples > 1000) {
$samples = 1000;
}
// Get the actual data.
$usage = $adminhelper->get_usage($samples);
// Set up the renderer and organise data to render.
$renderer = $PAGE->get_renderer('core_cache');
[$table, $summarytable] = $renderer->usage_tables($usage);
$form = new \core_cache\output\usage_samples_form();
echo $OUTPUT->header();
echo $renderer->usage_page($table, $summarytable, $form);
echo $OUTPUT->footer();