diff --git a/cache/stores/apcu/addinstanceform.php b/cache/stores/apcu/addinstanceform.php new file mode 100644 index 00000000000..f9fc73e7e66 --- /dev/null +++ b/cache/stores/apcu/addinstanceform.php @@ -0,0 +1,85 @@ +. +/** + * The library file for the apcu cache store. + * + * This file is part of the apcu cache store, it contains the API for interacting with an instance of the store. + * + * @package cachestore_apcu + * @copyright 2014 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot.'/cache/forms.php'); +/** + * Form for adding a apcu instance. + * + * @copyright 2014 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cachestore_apcu_addinstance_form extends cachestore_addinstance_form { + /** + * Add the desired form elements. + */ + protected function configuration_definition() { + global $CFG; + $form = $this->_form; + $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_apcu'), + array('maxlength' => 5, 'size' => 5)); + $form->addHelpButton('prefix', 'prefix', 'cachestore_apcu'); + $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext. + $form->setDefault('prefix', $CFG->prefix); + $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_apcu'), 'regex', '#^[a-zA-Z0-9\-_]+$#'); + $form->addElement('header', 'apc_notice', get_string('notice', 'cachestore_apcu')); + $form->setExpanded('apc_notice'); + $link = get_docs_url('Caching#APC'); + $form->addElement('html', nl2br(get_string('clusternotice', 'cachestore_apcu', $link))); + } + + /** + * Validates the configuration data. + * + * We need to check that prefix is unique. + * + * @param array $data + * @param array $files + * @param array $errors + * @return array + * @throws coding_exception + */ + public function configuration_validation($data, $files, array $errors) { + if (empty($errors['prefix'])) { + $factory = cache_factory::instance(); + $config = $factory->create_config_instance(); + foreach ($config->get_all_stores() as $store) { + if ($store['plugin'] === 'apcu') { + if (isset($store['configuration']['prefix'])) { + if ($data['prefix'] === $store['configuration']['prefix']) { + // The new store has the same prefix as an existing store, thats a problem. + $errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu'); + break; + } + } else if (empty($data['prefix'])) { + // The existing store hasn't got a prefix and neither does the new store, that's a problem. + $errors['prefix'] = get_string('prefixnotunique', 'cachestore_apcu'); + break; + } + } + } + } + return $errors; + } +} diff --git a/cache/stores/apcu/lang/en/cachestore_apcu.php b/cache/stores/apcu/lang/en/cachestore_apcu.php new file mode 100644 index 00000000000..95608a609ad --- /dev/null +++ b/cache/stores/apcu/lang/en/cachestore_apcu.php @@ -0,0 +1,34 @@ +. + +/** + * APCu cache store language strings. + * + * @package cachestore_apcu + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['clusternotice'] = 'Please be aware that APCu only a suitable choice for single node sites or caches that can be stored locally. +For more information see Moodle docs'; +$string['notice'] = 'Notice'; +$string['pluginname'] = 'APC User Cache (APCu)'; +$string['prefix'] = 'Prefix'; +$string['prefix_help'] = 'The above prefix gets used for all keys being stored in this APC store instance. By default the database prefix is used.'; +$string['prefixinvalid'] = 'The prefix you have selected is invalid. You can only use a-z A-Z 0-9-_.'; +$string['prefixnotunique'] = 'The prefix you have selected is not unique. Please choose a unique prefix.'; +$string['testperformance'] = 'Test performance'; +$string['testperformance_desc'] = 'If enabled APCu performance will be included when viewing the Test performance page in the administration block. Enabling this on a production site is not recommended.'; diff --git a/cache/stores/apcu/lib.php b/cache/stores/apcu/lib.php new file mode 100644 index 00000000000..a0dd6b8fced --- /dev/null +++ b/cache/stores/apcu/lib.php @@ -0,0 +1,418 @@ +. + +/** + * APCu cache store main library. + * + * @package cachestore_apcu + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * The APCu cache store class. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cachestore_apcu extends cache_store implements cache_is_key_aware, cache_is_configurable { + + /** + * The required version of APCu for this extension. + */ + const REQUIRED_VERSION = '4.0.0'; + + /** + * The name of this store instance. + * @var string + */ + protected $name; + + /** + * The definition used when this instance was initialised. + * @var cache_definition + */ + protected $definition = null; + + /** + * The storeprefix to use on all instances of this store. Configured as part store setup. + * @var string + */ + protected $storeprefix = null; + + /** + * The prefix added specifically for this cache. + * @var string + */ + protected $cacheprefix = null; + + /** + * Static method to check that the APCu stores requirements have been met. + * + * It checks that the APCu extension has been loaded and that it has been enabled. + * + * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise. + */ + public static function are_requirements_met() { + if (!extension_loaded('apcu') || !ini_get('apc.enabled')) { + return false; + } + + $version = phpversion('apcu'); + return $version && version_compare($version, self::REQUIRED_VERSION, '>='); + } + + /** + * Static method to check if a store is usable with the given mode. + * + * @param int $mode One of cache_store::MODE_* + * @return bool True if the mode is supported. + */ + public static function is_supported_mode($mode) { + return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); + } + + /** + * Returns the supported features as a binary flag. + * + * @param array $configuration The configuration of a store to consider specifically. + * @return int The supported features. + */ + public static function get_supported_features(array $configuration = array()) { + return self::SUPPORTS_NATIVE_TTL; + } + + /** + * Returns the supported modes as a binary flag. + * + * @param array $configuration The configuration of a store to consider specifically. + * @return int The supported modes. + */ + public static function get_supported_modes(array $configuration = array()) { + return self::MODE_APPLICATION + self::MODE_SESSION; + } + + /** + * Constructs an instance of the cache store. + * + * This method should not create connections or perform and processing, it should be used + * + * @param string $name The name of the cache store + * @param array $configuration The configuration for this store instance. + */ + public function __construct($name, array $configuration = array()) { + global $CFG; + $this->name = $name; + $this->storeprefix = $CFG->prefix; + if (isset($configuration['prefix'])) { + $this->storeprefix = $configuration['prefix']; + } + } + + /** + * Returns the name of this store instance. + * @return string + */ + public function my_name() { + return $this->name; + } + + /** + * Initialises a new instance of the cache store given the definition the instance is to be used for. + * + * This function should prepare any given connections etc. + * + * @param cache_definition $definition + * @return bool + */ + public function initialise(cache_definition $definition) { + $this->definition = $definition; + $this->cacheprefix = $this->storeprefix.$definition->generate_definition_hash().'__'; + return true; + } + + /** + * Returns true if this cache store instance has been initialised. + * @return bool + */ + public function is_initialised() { + return ($this->definition !== null); + } + + /** + * Returns true if this cache store instance is ready to use. + * @return bool + */ + public function is_ready() { + // No set up is actually required, providing apc is installed and enabled. + return true; + } + + /** + * Prepares the given key for use. + * + * Should be called before all interaction. + * + * @param string $key The key to prepare for storing in APCu. + * + * @return string + */ + protected function prepare_key($key) { + return $this->cacheprefix . $key; + } + + /** + * Retrieves an item from the cache store given its key. + * + * @param string $key The key to retrieve + * @return mixed The data that was associated with the key, or false if the key did not exist. + */ + public function get($key) { + $key = $this->prepare_key($key); + $success = false; + $outcome = apcu_fetch($key, $success); + if ($success) { + return $outcome; + } + return $success; + } + + /** + * Retrieves several items from the cache store in a single transaction. + * + * If not all of the items are available in the cache then the data value for those that are missing will be set to false. + * + * @param array $keys The array of keys to retrieve + * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will + * be set to false. + */ + public function get_many($keys) { + $map = array(); + foreach ($keys as $key) { + $map[$key] = $this->prepare_key($key); + } + $outcomes = array(); + $success = false; + $results = apcu_fetch($map, $success); + if ($success) { + foreach ($map as $key => $used) { + if (array_key_exists($used, $results)) { + $outcomes[$key] = $results[$used]; + } else { + $outcomes[$key] = false; + } + } + } else { + $outcomes = array_fill_keys($keys, false); + } + return $outcomes; + } + + /** + * Sets an item in the cache given its key and data value. + * + * @param string $key The key to use. + * @param mixed $data The data to set. + * @return bool True if the operation was a success false otherwise. + */ + public function set($key, $data) { + $key = $this->prepare_key($key); + return apcu_store($key, $data, $this->definition->get_ttl()); + } + + /** + * Sets many items in the cache in a single transaction. + * + * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two + * keys, 'key' and 'value'. + * @return int The number of items successfully set. It is up to the developer to check this matches the number of items + * sent ... if they care that is. + */ + public function set_many(array $keyvaluearray) { + $map = array(); + foreach ($keyvaluearray as $pair) { + $key = $this->prepare_key($pair['key']); + $map[$key] = $pair['value']; + } + $result = apcu_store($map, null, $this->definition->get_ttl()); + return count($map) - count($result); + } + + /** + * Deletes an item from the cache store. + * + * @param string $key The key to delete. + * @return bool Returns true if the operation was a success, false otherwise. + */ + public function delete($key) { + $key = $this->prepare_key($key); + return apcu_delete($key); + } + + /** + * Deletes several keys from the cache in a single action. + * + * @param array $keys The keys to delete + * @return int The number of items successfully deleted. + */ + public function delete_many(array $keys) { + $count = 0; + foreach ($keys as $key) { + if ($this->delete($key)) { + $count++; + } + } + return $count; + } + + /** + * Purges the cache deleting all items within it. + * + * @return boolean True on success. False otherwise. + */ + public function purge() { + if (class_exists('APCUIterator', false)) { + $iterator = new APCUIterator('#^' . preg_quote($this->cacheprefix, '#') . '#'); + } else { + $iterator = new APCIterator('user', '#^' . preg_quote($this->cacheprefix, '#') . '#'); + } + return apcu_delete($iterator); + } + + /** + * Performs any necessary clean up when the store instance is being deleted. + */ + public function instance_deleted() { + if (class_exists('APCUIterator', false)) { + $iterator = new APCUIterator('#^' . preg_quote($this->storeprefix, '#') . '#'); + } else { + $iterator = new APCIterator('user', '#^' . preg_quote($this->storeprefix, '#') . '#'); + } + return apcu_delete($iterator); + } + + /** + * Generates an instance of the cache store that can be used for testing. + * + * Returns an instance of the cache store, or false if one cannot be created. + * + * @param cache_definition $definition + * @return cache_store + */ + public static function initialise_test_instance(cache_definition $definition) { + $testperformance = get_config('cachestore_apcu', 'testperformance'); + if (empty($testperformance)) { + return false; + } + if (!self::are_requirements_met()) { + return false; + } + $name = 'APCu test'; + $cache = new cachestore_apcu($name); + $cache->initialise($definition); + return $cache; + } + + /** + * Test is a cache has a key. + * + * @param string|int $key + * @return bool True if the cache has the requested key, false otherwise. + */ + public function has($key) { + $key = $this->prepare_key($key); + return apcu_exists($key); + } + + /** + * Test if a cache has at least one of the given keys. + * + * @param array $keys + * @return bool True if the cache has at least one of the given keys + */ + public function has_any(array $keys) { + foreach ($keys as $arraykey => $key) { + $keys[$arraykey] = $this->prepare_key($key); + } + $result = apcu_exists($keys); + return count($result) > 0; + } + + /** + * Test is a cache has all of the given keys. + * + * @param array $keys + * @return bool True if the cache has all of the given keys, false otherwise. + */ + public function has_all(array $keys) { + foreach ($keys as $arraykey => $key) { + $keys[$arraykey] = $this->prepare_key($key); + } + $result = apcu_exists($keys); + return count($result) === count($keys); + } + + /** + * Generates an instance of the cache store that can be used for testing. + * + * @param cache_definition $definition + * @return cachestore_apcu|false + */ + public static function initialise_unit_test_instance(cache_definition $definition) { + if (!self::are_requirements_met()) { + return false; + } + + $store = new cachestore_apcu('Test APCu', array('prefix' => 'phpunit')); + if (!$store->is_ready()) { + return false; + } + $store->initialise($definition); + + return $store; + } + + /** + * Given the data from the add instance form this function creates a configuration array. + * + * @param stdClass $data + * @return array + */ + public static function config_get_configuration_array($data) { + $config = array(); + + if (isset($data->prefix)) { + $config['prefix'] = $data->prefix; + } + return $config; + } + /** + * Allows the cache store to set its data against the edit form before it is shown to the user. + * + * @param moodleform $editform + * @param array $config + */ + public static function config_set_edit_form_data(moodleform $editform, array $config) { + if (isset($config['prefix'])) { + $data['prefix'] = $config['prefix']; + } else { + $data['prefix'] = ''; + } + $editform->set_data($data); + } +} diff --git a/cache/stores/apcu/settings.php b/cache/stores/apcu/settings.php new file mode 100644 index 00000000000..0b35e951467 --- /dev/null +++ b/cache/stores/apcu/settings.php @@ -0,0 +1,36 @@ +. + +/** + * The settings for the APCu store. + * + * This file is part of the APCu cache store, it contains the API for interacting with an instance of the store. + * + * @package cachestore_apcu + * @copyright 2014 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$settings->add( + new admin_setting_configcheckbox( + 'cachestore_apcu/testperformance', + new lang_string('testperformance', 'cachestore_apcu'), + new lang_string('testperformance_desc', 'cachestore_apcu'), + false + ) +); diff --git a/cache/stores/apcu/tests/apcu_test.php b/cache/stores/apcu/tests/apcu_test.php new file mode 100644 index 00000000000..36fc1ae8c3e --- /dev/null +++ b/cache/stores/apcu/tests/apcu_test.php @@ -0,0 +1,87 @@ +. + +/** + * APCu unit tests. + * + * @package cachestore_apcu + * @copyright 2014 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +// Include the necessary evils. +global $CFG; +require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php'); +require_once($CFG->dirroot.'/cache/stores/apcu/lib.php'); + +/** + * APC unit test class. + * + * @package cachestore_apcu + * @copyright 2014 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cachestore_apcu_test extends cachestore_tests { + /** + * Returns the apcu class name + * @return string + */ + protected function get_class_name() { + return 'cachestore_apcu'; + } + + public function setUp() { + if (!cachestore_apcu::are_requirements_met()) { + $this->markTestSkipped('Could not test cachestore_apcu. Requirements are not met.'); + } + return parent::setUp(); + } + + /** + * Test that the Moodle APCu store doesn't cross paths with other code using APCu as well. + */ + public function test_cross_application_interaction() { + $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test'); + $instance = cachestore_apcu::initialise_unit_test_instance($definition); + + // Test purge with custom data. + $this->assertTrue($instance->set('test', 'monster')); + $this->assertSame('monster', $instance->get('test')); + $this->assertTrue(apcu_store('test', 'pirate', 180)); + $this->assertSame('monster', $instance->get('test')); + $this->assertTrue(apcu_exists('test')); + $this->assertSame('pirate', apcu_fetch('test')); + // Purge and check that our data is gone but the the custom data is still there. + $this->assertTrue($instance->purge()); + $this->assertFalse($instance->get('test')); + $this->assertTrue(apcu_exists('test')); + $this->assertSame('pirate', apcu_fetch('test')); + } + + public function test_different_caches_have_different_prefixes() { + $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test'); + $instance = cachestore_apcu::initialise_unit_test_instance($definition); + $definition2 = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_apcu', 'phpunit_test2'); + $instance2 = cachestore_apcu::initialise_unit_test_instance($definition2); + + $instance->set('test1', 1); + $this->assertFalse($instance2->get('test1')); + $instance2->purge(); + $this->assertSame(1, $instance->get('test1')); + } +} \ No newline at end of file diff --git a/cache/stores/apcu/version.php b/cache/stores/apcu/version.php new file mode 100644 index 00000000000..91d983a92e3 --- /dev/null +++ b/cache/stores/apcu/version.php @@ -0,0 +1,30 @@ +. + +/** + * APCu cache store version information. + * + * @package cachestore_apcu + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2016081600; +$plugin->requires = 2016081100; +$plugin->maturity = MATURITY_STABLE; +$plugin->component = 'cachestore_apcu'; diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index dde3c25c67e..ac8b0183fe1 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1744,7 +1744,7 @@ class core_plugin_manager { ), 'cachestore' => array( - 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static' + 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static', 'apcu' ), 'calendartype' => array(