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
+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;
}
}