MDL-48506 cachestore_memcached: is now multi-site safe

This commit is contained in:
Sam Hemelryk
2014-12-11 16:42:21 +13:00
committed by Ryan Wyllie
parent 4f33514063
commit 957e0c7567
2 changed files with 155 additions and 9 deletions

View File

@@ -44,6 +44,12 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cachestore_memcached extends cache_store implements cache_is_configurable {
/**
* The minimum required version of memcached in order to use this store.
*/
const REQUIRED_VERSION = '2.0.0';
/**
* The name of the store
* @var store
@@ -104,6 +110,26 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
*/
protected $setconnections = array();
/**
* The prefix to use on all keys.
* @var string
*/
protected $prefix = '';
/**
* True if Memcached::deleteMulti can be used, false otherwise.
* This required extension version 2.0.0 or greater.
* @var bool
*/
protected $candeletemulti = false;
/**
* True if Memcached::getAllKeys can be used, false otherwise.
* This required extension version 2.0.0 or greater.
* @var bool
*/
protected $cangetallkeys = false;
/**
* Constructs the store instance.
*
@@ -171,7 +197,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
$this->options[Memcached::OPT_COMPRESSION] = $compression;
$this->options[Memcached::OPT_SERIALIZER] = $serialiser;
$this->options[Memcached::OPT_PREFIX_KEY] = $prefix;
$this->options[Memcached::OPT_PREFIX_KEY] = $this->prefix = (string)$prefix;
$this->options[Memcached::OPT_HASH] = $hashmethod;
$this->options[Memcached::OPT_BUFFER_WRITES] = $bufferwrites;
@@ -196,6 +222,10 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
}
}
$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);
}
@@ -205,6 +235,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
*
* Once this has been done the cache is all set to be used.
*
* @throws coding_exception if the instance has already been initialised.
* @param cache_definition $definition
*/
public function initialise(cache_definition $definition) {
@@ -238,7 +269,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
* @return bool
*/
public static function are_requirements_met() {
return class_exists('Memcached');
return extension_loaded('memcached') && class_exists('Memcached');
}
/**
@@ -411,6 +442,18 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
*/
protected function delete_many_connection(Memcached $connection, array $keys) {
$count = 0;
if ($this->candeletemulti) {
// We can use deleteMulti, this is a bit faster yay!
$result = $connection->deleteMulti($keys);
foreach ($result as $key => $outcome) {
if ($outcome === true) {
$count++;
}
}
return $count;
}
// They are running an older version of the php memcached extension.
foreach ($keys as $key) {
if ($connection->delete($key)) {
$count++;
@@ -428,16 +471,47 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
if ($this->isready) {
if ($this->clustered) {
foreach ($this->setconnections as $connection) {
if ($this->candeletemulti && $this->cangetallkeys) {
$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) {
$keys = self::get_prefixed_keys($this->connection, $this->prefix);
$this->connection->deleteMulti($keys);
} else {
// Oh damn, this isn't multi-site safe.
$this->connection->flush();
}
}
// It never fails. Ever.
return true;
}
/**
* 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
* @return array
*/
protected static function get_prefixed_keys(Memcached $connection, $prefix) {
$keys = array();
$start = strlen($prefix);
foreach ($connection->getAllKeys() as $key) {
if (strpos($key, $prefix) === 0) {
$keys[] = substr($key, $start);
}
}
return $keys;
}
/**
* Gets an array of options to use as the serialiser.
* @return array
@@ -585,6 +659,8 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
$connection->addServers($this->servers);
}
}
// We have to flush here to be sure we are completely cleaned up.
// Bad for performance but this is incredibly rare.
@$connection->flush();
unset($connection);
unset($this->connection);

View File

@@ -54,6 +54,10 @@ class cachestore_memcached_test extends cachestore_tests {
* Tests the valid keys to ensure they work.
*/
public function test_valid_keys() {
if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
}
$this->resetAfterTest(true);
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
@@ -139,16 +143,16 @@ class cachestore_memcached_test extends cachestore_tests {
* Tests the clustering feature.
*/
public function test_clustered() {
$this->resetAfterTest(true);
if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
$this->markTestSkipped();
if (!cachestore_memcached::are_requirements_met() || !defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
$this->markTestSkipped('Could not test cachestore_memcached. Requirements are not met.');
}
$this->resetAfterTest(true);
$testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
if (count($testservers) < 2) {
$this->markTestSkipped();
$this->markTestSkipped('Could not test clustered memcached, there are not enough test servers defined.');
}
// Use the first server as our primary.
@@ -270,4 +274,70 @@ class cachestore_memcached_test extends cachestore_tests {
}
}
}
/**
* Tests that memcached cache store doesn't just flush everything and instead deletes only what belongs to it.
*/
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);
$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 not purged.
$this->assertTrue($cachestore->purge());
$this->assertFalse($cachestore->get('test'));
$this->assertSame('connection', $connection->get('test'));
}
/**
* Given a server string this returns an array of servers.
*
* @param string $serverstring
* @return array
*/
public function get_servers($serverstring) {
$servers = array();
foreach (explode("\n", $serverstring) as $server) {
if (!is_array($server)) {
$server = explode(':', $server, 3);
}
if (!array_key_exists(1, $server)) {
$server[1] = 11211;
$server[2] = 100;
} else if (!array_key_exists(2, $server)) {
$server[2] = 100;
}
$servers[] = $server;
}
return $servers;
}
}