diff --git a/cache/stores/memcached/lib.php b/cache/stores/memcached/lib.php index 5e8250d1633..82ded7da209 100644 --- a/cache/stores/memcached/lib.php +++ b/cache/stores/memcached/lib.php @@ -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) { - $connection->flush(); + 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); diff --git a/cache/stores/memcached/tests/memcached_test.php b/cache/stores/memcached/tests/memcached_test.php index 506e583c01c..6dd0e82a390 100644 --- a/cache/stores/memcached/tests/memcached_test.php +++ b/cache/stores/memcached/tests/memcached_test.php @@ -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; + } }