Skip to content

Commit

Permalink
MDL-48506 cachestore_memcached: added shared cache config option
Browse files Browse the repository at this point in the history
When added a memcached instance you can now select whether the cache is
being shared by other applications. The setting will determine the
purging strategy.

Shared caches will have individual keys deleted while dedicated caches
will have the entire cache purged (better performance over networks).

Note: This option only works with the correct version of the php
memcached extension and with the multi-site safe changes.
  • Loading branch information
ryanwyllie committed Apr 18, 2016
1 parent 7797d7e commit 33688fb
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 13 deletions.
14 changes: 12 additions & 2 deletions cache/stores/memcached/addinstanceform.php
Expand Up @@ -42,6 +42,8 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
*/
protected function configuration_definition() {
$form = $this->_form;
$version = phpversion('memcached');
$hasrequiredversion = ($version || version_compare($version, cachestore_memcached::REQUIRED_VERSION, '>='));

$form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcached'), array('cols' => 75, 'rows' => 5));
$form->addHelpButton('servers', 'servers', 'cachestore_memcached');
Expand Down Expand Up @@ -75,6 +77,15 @@ protected function configuration_definition() {
$form->setDefault('bufferwrites', 0);
$form->setType('bufferwrites', PARAM_BOOL);

if ($hasrequiredversion) {
// Only show this option if we have the required version of memcache extension installed.
// If it's not installed then this option does nothing, so there is no point in displaying it.
$form->addElement('selectyesno', 'isshared', get_string('isshared', 'cachestore_memcached'));
$form->addHelpButton('isshared', 'isshared', 'cachestore_memcached');
$form->setDefault('isshared', 0);
$form->setType('isshared', PARAM_BOOL);
}

$form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcached'));

$form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcached'));
Expand All @@ -87,8 +98,7 @@ protected function configuration_definition() {
$form->disabledIf('setservers', 'clustered');
$form->setType('setservers', PARAM_RAW);

$version = phpversion('memcached');
if (!$version || !version_compare($version, cachestore_memcached::REQUIRED_VERSION, '>=')) {
if (!$hasrequiredversion) {
$form->addElement('header', 'upgradenotice', get_string('notice', 'cachestore_memcached'));
$form->setExpanded('upgradenotice');
$form->addElement('html', nl2br(get_string('upgrade200recommended', 'cachestore_memcached')));
Expand Down
7 changes: 7 additions & 0 deletions cache/stores/memcached/lang/en/cachestore_memcached.php
Expand Up @@ -46,6 +46,13 @@
$string['hash_fnv1a_32'] = 'FNV1A_32';
$string['hash_hsieh'] = 'Hsieh';
$string['hash_murmur'] = 'Murmur';
$string['isshared'] = 'Shared cache';
$string['isshared_help'] = "Is your memcached server also being used by other applications?
If the cache is shared by other applications then each key will be deleted individually to ensure that only data owned by this application is purged (leaving external application cache data unchanged). This can result in reduced performance when purging the cache, depending on your server configuration.
If you are running a dedicated cache for this application then the entire cache can safely be flushed without any risk of destroying another application's cache data. This should result in increased performance when purging the cache.
";
$string['notice'] = 'Notice';
$string['pluginname'] = 'Memcached';
$string['prefix'] = 'Prefix key';
Expand Down
29 changes: 22 additions & 7 deletions cache/stores/memcached/lib.php
Expand Up @@ -124,11 +124,11 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
protected $candeletemulti = false;

/**
* True if Memcached::getAllKeys can be used, false otherwise.
* True if the memcached server is shared, false otherwise.
* This required extension version 2.0.0 or greater.
* @var bool
*/
protected $cangetallkeys = false;
protected $isshared = false;

/**
* Constructs the store instance.
Expand Down Expand Up @@ -222,9 +222,12 @@ public function __construct($name, array $configuration = array()) {
}
}

if (isset($configuration['isshared'])) {
$this->isshared = $configuration['isshared'];
}

$version = phpversion('memcached');
$this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
$this->cangetallkeys = $this->candeletemulti;

// Test the connection to the main connection.
$this->isready = @$this->connection->set("ping", 'ping', 1);
Expand Down Expand Up @@ -469,17 +472,21 @@ protected function delete_many_connection(Memcached $connection, array $keys) {
*/
public function purge() {
if ($this->isready) {
// Only use delete multi if we have the correct extension installed and if the memcached
// server is shared (flushing the cache is quicker otherwise).
$candeletemulti = ($this->candeletemulti && $this->isshared);

if ($this->clustered) {
foreach ($this->setconnections as $connection) {
if ($this->candeletemulti && $this->cangetallkeys) {
if ($candeletemulti) {
$keys = self::get_prefixed_keys($connection, $this->prefix);
$connection->deleteMulti($keys);
} else {
// Oh damn, this isn't multi-site safe.
$connection->flush();
}
}
} else if ($this->candeletemulti && $this->cangetallkeys) {
} else if ($candeletemulti) {
$keys = self::get_prefixed_keys($this->connection, $this->prefix);
$this->connection->deleteMulti($keys);
} else {
Expand All @@ -495,7 +502,6 @@ public function purge() {
* Returns all of the keys in the given connection that belong to this cache store instance.
*
* Requires php memcached extension version 2.0.0 or greater.
* You should always check $this->cangetallkeys before calling this.
*
* @param Memcached $connection
* @param string $prefix
Expand Down Expand Up @@ -588,6 +594,11 @@ public static function config_get_configuration_array($data) {
}
}

$isshared = false;
if (isset($data->isshared)) {
$isshared = $data->isshared;
}

return array(
'servers' => $servers,
'compression' => $data->compression,
Expand All @@ -596,7 +607,8 @@ public static function config_get_configuration_array($data) {
'hash' => $data->hash,
'bufferwrites' => $data->bufferwrites,
'clustered' => $clustered,
'setservers' => $setservers
'setservers' => $setservers,
'isshared' => $isshared
);
}

Expand Down Expand Up @@ -640,6 +652,9 @@ public static function config_set_edit_form_data(moodleform $editform, array $co
}
$data['setservers'] = join("\n", $servers);
}
if (isset($config['isshared'])) {
$data['isshared'] = $config['isshared'];
}
$editform->set_data($data);
}

Expand Down
72 changes: 70 additions & 2 deletions cache/stores/memcached/tests/memcached_test.php
Expand Up @@ -276,15 +276,16 @@ public function test_clustered() {
}

/**
* Tests that memcached cache store doesn't just flush everything and instead deletes only what belongs to it.
* Tests that memcached cache store doesn't just flush everything and instead deletes only what belongs to it
* when it is marked as a shared cache.
*/
public function test_multi_use_compatibility() {
if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
}

$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
$cachestore = cachestore_memcached::initialise_unit_test_instance($definition);
$cachestore = $this->create_test_cache_with_config($definition, array('isshared' => true));
$connection = new Memcached(crc32(__METHOD__));
$connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
$connection->setOptions(array(
Expand Down Expand Up @@ -318,6 +319,49 @@ public function test_multi_use_compatibility() {
$this->assertSame('connection', $connection->get('test'));
}

/**
* Tests that memcached cache store flushes entire cache when it is using a dedicated cache.
*/
public function test_dedicated_cache() {
if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
}

$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
$cachestore = $this->create_test_cache_with_config($definition, array('isshared' => false));
$connection = new Memcached(crc32(__METHOD__));
$connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
$connection->setOptions(array(
Memcached::OPT_COMPRESSION => true,
Memcached::OPT_SERIALIZER => Memcached::SERIALIZER_PHP,
Memcached::OPT_PREFIX_KEY => 'phpunit_',
Memcached::OPT_BUFFER_WRITES => false
));

// We must flush first to make sure nothing is there.
$connection->flush();

// Test the cachestore.
$this->assertFalse($cachestore->get('test'));
$this->assertTrue($cachestore->set('test', 'cachestore'));
$this->assertSame('cachestore', $cachestore->get('test'));

// Test the connection.
$this->assertFalse($connection->get('test'));
$this->assertEquals(Memcached::RES_NOTFOUND, $connection->getResultCode());
$this->assertTrue($connection->set('test', 'connection'));
$this->assertSame('connection', $connection->get('test'));

// Test both again and make sure the values are correct.
$this->assertSame('cachestore', $cachestore->get('test'));
$this->assertSame('connection', $connection->get('test'));

// Purge the cachestore and check the connection was also purged.
$this->assertTrue($cachestore->purge());
$this->assertFalse($cachestore->get('test'));
$this->assertFalse($connection->get('test'));
}

/**
* Given a server string this returns an array of servers.
*
Expand All @@ -340,4 +384,28 @@ public function get_servers($serverstring) {
}
return $servers;
}

/**
* Creates a test instance for unit tests.
* @param cache_definition $definition
* @param array $configuration
* @return null|cachestore_memcached
*/
private function create_test_cache_with_config(cache_definition $definition, $configuration = array()) {
$class = $this->get_class_name();

if (!$class::are_requirements_met()) {
return null;
}
if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
return null;
}

$configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);

$store = new $class('Test memcached', $configuration);
$store->initialise($definition);

return $store;
}
}
2 changes: 0 additions & 2 deletions cache/upgrade.txt
Expand Up @@ -6,8 +6,6 @@ Information provided here is intended especially for developers.
This allows the cache loader to decide if it needs to handle dereferencing or whether the data
coming directly to it has already had references resolved.
- see supports_dereferencing_objects in store.php.
* The Memcached cache store no longer flushes the memcache servers it is connected to providing your memcached php extension is version 2.0.0 or greater.
There is a notice to this effect when adding or editing a memcached cache store instance.

=== 2.9 ===
* Cache data source aggregation functionality has been removed. This functionality was found to be broken and unused.
Expand Down

0 comments on commit 33688fb

Please sign in to comment.