Merge branch 'MDL-42071_MK3' of https://github.com/merrill-oakland/moodle
This commit is contained in:
+44
@@ -52,5 +52,49 @@ class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
|
||||
$form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
|
||||
$form->setDefault('prefix', 'mdl_');
|
||||
$form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
|
||||
|
||||
$form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcache'));
|
||||
|
||||
$form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcache'));
|
||||
$form->setDefault('checkbox', false);
|
||||
$form->addHelpButton('clustered', 'clustered', 'cachestore_memcache');
|
||||
|
||||
$form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcache'),
|
||||
array('cols' => 75, 'rows' => 5));
|
||||
$form->addHelpButton('setservers', 'setservers', 'cachestore_memcache');
|
||||
$form->disabledIf('setservers', 'clustered');
|
||||
$form->setType('setservers', PARAM_RAW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform minimal validation on the settings form.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
*/
|
||||
public function validation($data, $files) {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
if (isset($data['clustered']) && ($data['clustered'] == 1)) {
|
||||
// Set servers is required with in cluster mode.
|
||||
if (!isset($data['setservers']) || empty(trim($data['setservers']))) {
|
||||
$errors['setservers'] = get_string('required');
|
||||
}
|
||||
|
||||
$validservers = false;
|
||||
if (isset($data['servers'])) {
|
||||
$servers = trim($data['servers']);
|
||||
$servers = explode("\n", $servers);
|
||||
if (count($servers) === 1) {
|
||||
$validservers = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$validservers) {
|
||||
$errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcache');
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,13 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['clustered'] = 'Enable clustered servers';
|
||||
$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
|
||||
|
||||
The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
|
||||
|
||||
When this setting is enabled, the server listed above will be used for fetching.';
|
||||
$string['clusteredheader'] = 'Split servers';
|
||||
$string['pluginname'] = 'Memcache';
|
||||
$string['prefix'] = 'Key prefix';
|
||||
$string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
|
||||
@@ -42,6 +49,20 @@ For example:
|
||||
server.url.com
|
||||
ipaddress:port
|
||||
servername:port:weight
|
||||
</pre>
|
||||
|
||||
If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local manchine, like 127.0.0.1 or localhost.';
|
||||
$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
|
||||
$string['setservers'] = 'Set Servers';
|
||||
$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
|
||||
It **must** include the server listed in *Servers* above, even if by a different hostname.
|
||||
Servers should be defined one per line and consist of a server address and optionally a port.
|
||||
If no port is provided then the default port (11211) is used.
|
||||
|
||||
For example:
|
||||
<pre>
|
||||
server.url.com
|
||||
ipaddress:port
|
||||
</pre>';
|
||||
$string['testservers'] = 'Test servers';
|
||||
$string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
|
||||
|
||||
Vendored
+117
-2
@@ -87,6 +87,24 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
*/
|
||||
protected $definition;
|
||||
|
||||
/**
|
||||
* Set to true when this store is clustered.
|
||||
* @var bool
|
||||
*/
|
||||
protected $clustered = false;
|
||||
|
||||
/**
|
||||
* Array of servers to set when in clustered mode.
|
||||
* @var array
|
||||
*/
|
||||
protected $setservers = array();
|
||||
|
||||
/**
|
||||
* The an array of memcache connections for the set servers, once established.
|
||||
* @var array
|
||||
*/
|
||||
protected $setconnections = array();
|
||||
|
||||
/**
|
||||
* Default prefix for key names.
|
||||
* @var string
|
||||
@@ -123,6 +141,30 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
}
|
||||
$this->servers[] = $server;
|
||||
}
|
||||
|
||||
$this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
|
||||
|
||||
if ($this->clustered) {
|
||||
if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
|
||||
// Can't setup clustering without set servers.
|
||||
return;
|
||||
}
|
||||
if (count($this->servers) !== 1) {
|
||||
// Can only setup cluster with exactly 1 get server.
|
||||
return;
|
||||
}
|
||||
foreach ($configuration['setservers'] as $server) {
|
||||
// We do not use weights (3rd part) on these servers.
|
||||
if (!is_array($server)) {
|
||||
$server = explode(':', $server, 3);
|
||||
}
|
||||
if (!array_key_exists(1, $server)) {
|
||||
$server[1] = 11211;
|
||||
}
|
||||
$this->setservers[] = $server;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($configuration['prefix'])) {
|
||||
$this->prefix = self::DEFAULT_PREFIX;
|
||||
} else {
|
||||
@@ -133,6 +175,16 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
foreach ($this->servers as $server) {
|
||||
$this->connection->addServer($server[0], (int) $server[1], true, (int) $server[2]);
|
||||
}
|
||||
|
||||
if ($this->clustered) {
|
||||
foreach ($this->setservers as $setserver) {
|
||||
// Since we will have a number of them with the same name, append server and port.
|
||||
$connection = new Memcache;
|
||||
$connection->addServer($setserver[0], $setserver[1]);
|
||||
$this->setconnections[] = $connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the connection to the pool of servers.
|
||||
$this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
|
||||
}
|
||||
@@ -280,6 +332,15 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
* @return bool True if the operation was a success false otherwise.
|
||||
*/
|
||||
public function set($key, $data) {
|
||||
if ($this->clustered) {
|
||||
$status = true;
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$status = $connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl())
|
||||
&& $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
|
||||
}
|
||||
|
||||
@@ -294,7 +355,7 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
public function set_many(array $keyvaluearray) {
|
||||
$count = 0;
|
||||
foreach ($keyvaluearray as $pair) {
|
||||
if ($this->connection->set($this->parse_key($pair['key']), $pair['value'], MEMCACHE_COMPRESSED, $this->definition->get_ttl())) {
|
||||
if ($this->set($pair['key'], $pair['value'])) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
@@ -308,6 +369,14 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
* @return bool Returns true if the operation was a success, false otherwise.
|
||||
*/
|
||||
public function delete($key) {
|
||||
if ($this->clustered) {
|
||||
$status = true;
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$status = $connection->delete($this->parse_key($key)) && $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $this->connection->delete($this->parse_key($key));
|
||||
}
|
||||
|
||||
@@ -334,7 +403,13 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
*/
|
||||
public function purge() {
|
||||
if ($this->isready) {
|
||||
$this->connection->flush();
|
||||
if ($this->clustered) {
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$connection->flush();
|
||||
}
|
||||
} else {
|
||||
$this->connection->flush();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -358,9 +433,33 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
}
|
||||
$servers[] = explode(':', $line, 3);
|
||||
}
|
||||
|
||||
$clustered = false;
|
||||
if (isset($data->clustered)) {
|
||||
$clustered = true;
|
||||
}
|
||||
|
||||
$lines = explode("\n", $data->setservers);
|
||||
$setservers = array();
|
||||
foreach ($lines as $line) {
|
||||
// Trim surrounding colons and default whitespace.
|
||||
$line = trim(trim($line), ":");
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$setserver = explode(':', $line, 3);
|
||||
// We don't use weights, so display a debug message.
|
||||
if (count($setserver) > 2) {
|
||||
debugging('Memcache Set Server '.$setserver[0].' has too many parameters.');
|
||||
}
|
||||
$setservers[] = $setserver;
|
||||
}
|
||||
|
||||
return array(
|
||||
'servers' => $servers,
|
||||
'prefix' => $data->prefix,
|
||||
'clustered' => $clustered,
|
||||
'setservers' => $setservers
|
||||
);
|
||||
}
|
||||
|
||||
@@ -384,6 +483,16 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
} else {
|
||||
$data['prefix'] = self::DEFAULT_PREFIX;
|
||||
}
|
||||
if (isset($config['clustered'])) {
|
||||
$data['clustered'] = (bool)$config['clustered'];
|
||||
}
|
||||
if (!empty($config['setservers'])) {
|
||||
$servers = array();
|
||||
foreach ($config['setservers'] as $server) {
|
||||
$servers[] = join(":", $server);
|
||||
}
|
||||
$data['setservers'] = join("\n", $servers);
|
||||
}
|
||||
|
||||
$editform->set_data($data);
|
||||
}
|
||||
@@ -423,6 +532,12 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
|
||||
|
||||
$configuration = array();
|
||||
$configuration['servers'] = explode("\n", $config->testservers);
|
||||
if (!empty($config->testclustered)) {
|
||||
$configuration['clustered'] = $config->testclustered;
|
||||
}
|
||||
if (!empty($config->testsetservers)) {
|
||||
$configuration['setservers'] = explode("\n", $config->testsetservers);
|
||||
}
|
||||
|
||||
$store = new cachestore_memcache('Test memcache', $configuration);
|
||||
$store->initialise($definition);
|
||||
|
||||
Vendored
+1
-1
@@ -30,4 +30,4 @@ $settings->add(new admin_setting_configtextarea(
|
||||
'cachestore_memcache/testservers',
|
||||
new lang_string('testservers', 'cachestore_memcache'),
|
||||
new lang_string('testservers_desc', 'cachestore_memcache'),
|
||||
'', PARAM_RAW, 60, 3));
|
||||
'', PARAM_RAW, 60, 3));
|
||||
|
||||
+180
@@ -54,6 +54,8 @@ class cachestore_memcache_test extends cachestore_tests {
|
||||
* Tests the valid keys to ensure they work.
|
||||
*/
|
||||
public function test_valid_keys() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
|
||||
$instance = cachestore_memcache::initialise_unit_test_instance($definition);
|
||||
|
||||
@@ -69,15 +71,193 @@ class cachestore_memcache_test extends cachestore_tests {
|
||||
// Underscores.
|
||||
'a_1', '1_a', '_a1', 'a1_'
|
||||
);
|
||||
|
||||
// Set some keys.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
|
||||
}
|
||||
|
||||
// Get some keys.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
|
||||
}
|
||||
|
||||
// Try get many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($values as $key => $value) {
|
||||
$this->assertEquals($key, $value);
|
||||
}
|
||||
|
||||
// Reset a key.
|
||||
$this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
|
||||
$this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
|
||||
$this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
|
||||
}
|
||||
|
||||
// Try set many, and check that count is correct.
|
||||
$many = array();
|
||||
foreach ($keys as $key) {
|
||||
$many[] = array('key' => $key, 'value' => $key);
|
||||
}
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Check keys retrieved with get_many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
|
||||
$this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
|
||||
}
|
||||
|
||||
// Delete many, make sure count matches.
|
||||
$returncount = $instance->delete_many($keys);
|
||||
$this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
|
||||
|
||||
// Check that each key was deleted.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
|
||||
}
|
||||
|
||||
// Set the keys again.
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Purge.
|
||||
$this->assertTrue($instance->purge(), 'Failure to purge');
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the clustering feature.
|
||||
*/
|
||||
public function test_clustered() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHE_TESTSERVERS));
|
||||
|
||||
if (count($testservers) < 2) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
// User the first server as our primary.
|
||||
set_config('testservers', $testservers[0], 'cachestore_memcache');
|
||||
set_config('testsetservers', TEST_CACHESTORE_MEMCACHE_TESTSERVERS, 'cachestore_memcache');
|
||||
set_config('testclustered', true, 'cachestore_memcache');
|
||||
|
||||
// First and instance that we can use to test the second server.
|
||||
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
|
||||
$instance = cachestore_memcache::initialise_test_instance($definition);
|
||||
|
||||
if (!$instance) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
// Now we are going to setup a connection to each independent server.
|
||||
set_config('testclustered', false, 'cachestore_memcache');
|
||||
set_config('testsetservers', '', 'cachestore_memcache');
|
||||
$checkinstances = array();
|
||||
foreach ($testservers as $testserver) {
|
||||
set_config('testservers', $testserver, 'cachestore_memcache');
|
||||
$checkinstance = cachestore_memcache::initialise_test_instance($definition);
|
||||
if (!$checkinstance) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
$checkinstances[] = $checkinstance;
|
||||
}
|
||||
|
||||
$keys = array(
|
||||
// Alphanumeric.
|
||||
'abc', 'ABC', '123', 'aB1', '1aB',
|
||||
// Hyphens.
|
||||
'a-1', '1-a', '-a1', 'a1-',
|
||||
// Underscores.
|
||||
'a_1', '1_a', '_a1', 'a1_'
|
||||
);
|
||||
|
||||
// Set each key.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
|
||||
}
|
||||
|
||||
// Check each key.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertEquals($key, $checkinstance->get($key), "Failed to get key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Reset a key.
|
||||
$this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
|
||||
$this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertEquals('New', $checkinstance->get($keys[0]), "Failed to get reset key `$key` from server $id");
|
||||
}
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
|
||||
$this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved deleted key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Try set many, and check that count is correct.
|
||||
$many = array();
|
||||
foreach ($keys as $key) {
|
||||
$many[] = array('key' => $key, 'value' => $key);
|
||||
}
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Check keys retrieved with get_many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
|
||||
$this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
|
||||
}
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$values = $checkinstance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key` from server $id");
|
||||
$this->assertEquals($key, $values[$key], "Failed to get_many key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete many, make sure count matches.
|
||||
$returncount = $instance->delete_many($keys);
|
||||
$this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
|
||||
|
||||
// Check that each key was deleted.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved many deleted key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Set the keys again.
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Purge.
|
||||
$this->assertTrue($instance->purge(), 'Failure to purge');
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved purged key `$key` from server 2");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -74,5 +74,49 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form
|
||||
$form->addHelpButton('bufferwrites', 'bufferwrites', 'cachestore_memcached');
|
||||
$form->setDefault('bufferwrites', 0);
|
||||
$form->setType('bufferwrites', PARAM_BOOL);
|
||||
|
||||
$form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcached'));
|
||||
|
||||
$form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcached'));
|
||||
$form->setDefault('checkbox', false);
|
||||
$form->addHelpButton('clustered', 'clustered', 'cachestore_memcached');
|
||||
|
||||
$form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcached'),
|
||||
array('cols' => 75, 'rows' => 5));
|
||||
$form->addHelpButton('setservers', 'setservers', 'cachestore_memcached');
|
||||
$form->disabledIf('setservers', 'clustered');
|
||||
$form->setType('setservers', PARAM_RAW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform minimal validation on the settings form.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $files
|
||||
*/
|
||||
public function validation($data, $files) {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
if (isset($data['clustered']) && ($data['clustered'] == 1)) {
|
||||
// Set servers is required with in cluster mode.
|
||||
if (!isset($data['setservers']) || empty(trim($data['setservers']))) {
|
||||
$errors['setservers'] = get_string('required');
|
||||
}
|
||||
|
||||
$validservers = false;
|
||||
if (isset($data['servers'])) {
|
||||
$servers = trim($data['servers']);
|
||||
$servers = explode("\n", $servers);
|
||||
if (count($servers) === 1) {
|
||||
$validservers = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$validservers) {
|
||||
$errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcached');
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,13 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['bufferwrites'] = 'Buffer writes';
|
||||
$string['bufferwrites_help'] = 'Enables or disables buffered I/O. Enabling buffered I/O causes storage commands to "buffer" instead of being sent. Any action that retrieves data causes this buffer to be sent to the remote connection. Quitting the connection or closing down the connection will also cause the buffered data to be pushed to the remote connection.';
|
||||
$string['clustered'] = 'Enable clustered servers';
|
||||
$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
|
||||
|
||||
The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
|
||||
|
||||
When this setting is enabled, the server listed above will be used for fetching.';
|
||||
$string['clusteredheader'] = 'Split servers';
|
||||
$string['hash'] = 'Hash method';
|
||||
$string['hash_help'] = 'Specifies the hashing algorithm used for the item keys. Each hash algorithm has its advantages and its disadvantages. Go with the default if you don\'t know or don\'t care.';
|
||||
$string['hash_default'] = 'Default (one-at-a-time)';
|
||||
@@ -56,6 +63,20 @@ For example:
|
||||
server.url.com
|
||||
ipaddress:port
|
||||
servername:port:weight
|
||||
</pre>
|
||||
|
||||
If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local manchine, like 127.0.0.1 or localhost.';
|
||||
$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
|
||||
$string['setservers'] = 'Set Servers';
|
||||
$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
|
||||
It **must** include the server listed in *Servers* above, even if by a different hostname.
|
||||
Servers should be defined one per line and consist of a server address and optionally a port.
|
||||
If no port is provided then the default port (11211) is used.
|
||||
|
||||
For example:
|
||||
<pre>
|
||||
server.url.com
|
||||
ipaddress:port
|
||||
</pre>';
|
||||
$string['testservers'] = 'Test servers';
|
||||
$string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
|
||||
|
||||
Vendored
+158
-6
@@ -86,6 +86,24 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
*/
|
||||
protected $definition;
|
||||
|
||||
/**
|
||||
* Set to true when this store is clustered.
|
||||
* @var bool
|
||||
*/
|
||||
protected $clustered = false;
|
||||
|
||||
/**
|
||||
* Array of servers to set when in clustered mode.
|
||||
* @var array
|
||||
*/
|
||||
protected $setservers = array();
|
||||
|
||||
/**
|
||||
* The an array of memcache connections for the set servers, once established.
|
||||
* @var array
|
||||
*/
|
||||
protected $setconnections = array();
|
||||
|
||||
/**
|
||||
* Constructs the store instance.
|
||||
*
|
||||
@@ -127,6 +145,30 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
}
|
||||
$this->servers[] = $server;
|
||||
}
|
||||
|
||||
$this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
|
||||
|
||||
if ($this->clustered) {
|
||||
if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
|
||||
// Can't setup clustering without set servers.
|
||||
return;
|
||||
}
|
||||
if (count($this->servers) !== 1) {
|
||||
// Can only setup cluster with exactly 1 get server.
|
||||
return;
|
||||
}
|
||||
foreach ($configuration['setservers'] as $server) {
|
||||
// We do not use weights (3rd part) on these servers.
|
||||
if (!is_array($server)) {
|
||||
$server = explode(':', $server, 3);
|
||||
}
|
||||
if (!array_key_exists(1, $server)) {
|
||||
$server[1] = 11211;
|
||||
}
|
||||
$this->setservers[] = $server;
|
||||
}
|
||||
}
|
||||
|
||||
$this->options[Memcached::OPT_COMPRESSION] = $compression;
|
||||
$this->options[Memcached::OPT_SERIALIZER] = $serialiser;
|
||||
$this->options[Memcached::OPT_PREFIX_KEY] = $prefix;
|
||||
@@ -141,7 +183,20 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
}
|
||||
$this->connection->addServers($this->servers);
|
||||
}
|
||||
// Test the connection to the pool of servers.
|
||||
|
||||
if ($this->clustered) {
|
||||
foreach ($this->setservers as $setserver) {
|
||||
// Since we will have a number of them with the same name, append server and port.
|
||||
$connection = new Memcached(crc32($this->name.$setserver[0].$setserver[1]));
|
||||
foreach ($this->options as $key => $value) {
|
||||
$connection->setOption($key, $value);
|
||||
}
|
||||
$connection->addServer($setserver[0], $setserver[1]);
|
||||
$this->setconnections[] = $connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the connection to the main connection.
|
||||
$this->isready = @$this->connection->set("ping", 'ping', 1);
|
||||
}
|
||||
|
||||
@@ -267,6 +322,14 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
* @return bool True if the operation was a success false otherwise.
|
||||
*/
|
||||
public function set($key, $data) {
|
||||
if ($this->clustered) {
|
||||
$status = true;
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$status = $connection->set($key, $data, $this->definition->get_ttl()) && $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $this->connection->set($key, $data, $this->definition->get_ttl());
|
||||
}
|
||||
|
||||
@@ -283,7 +346,17 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
foreach ($keyvaluearray as $pair) {
|
||||
$pairs[$pair['key']] = $pair['value'];
|
||||
}
|
||||
if ($this->connection->setMulti($pairs, $this->definition->get_ttl())) {
|
||||
|
||||
$status = true;
|
||||
if ($this->clustered) {
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$status = $connection->setMulti($pairs, $this->definition->get_ttl()) && $status;
|
||||
}
|
||||
} else {
|
||||
$status = $this->connection->setMulti($pairs, $this->definition->get_ttl());
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
return count($keyvaluearray);
|
||||
}
|
||||
return 0;
|
||||
@@ -296,6 +369,14 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
* @return bool Returns true if the operation was a success, false otherwise.
|
||||
*/
|
||||
public function delete($key) {
|
||||
if ($this->clustered) {
|
||||
$status = true;
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$status = $connection->delete($key) && $status;
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
return $this->connection->delete($key);
|
||||
}
|
||||
|
||||
@@ -306,9 +387,29 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
* @return int The number of items successfully deleted.
|
||||
*/
|
||||
public function delete_many(array $keys) {
|
||||
if ($this->clustered) {
|
||||
// Get the minimum deleted from any of the connections.
|
||||
$count = count($keys);
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$count = min($this->delete_many_connection($connection, $keys), $count);
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
return $this->delete_many_connection($this->connection, $keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes several keys from the cache in a single action for a specific connection.
|
||||
*
|
||||
* @param Memcached $connection The connection to work on.
|
||||
* @param array $keys The keys to delete
|
||||
* @return int The number of items successfully deleted.
|
||||
*/
|
||||
protected function delete_many_connection(Memcached $connection, array $keys) {
|
||||
$count = 0;
|
||||
foreach ($keys as $key) {
|
||||
if ($this->connection->delete($key)) {
|
||||
if ($connection->delete($key)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
@@ -322,7 +423,13 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
*/
|
||||
public function purge() {
|
||||
if ($this->isready) {
|
||||
$this->connection->flush();
|
||||
if ($this->clustered) {
|
||||
foreach ($this->setconnections as $connection) {
|
||||
$connection->flush();
|
||||
}
|
||||
} else {
|
||||
$this->connection->flush();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -382,6 +489,28 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
}
|
||||
$servers[] = explode(':', $line, 3);
|
||||
}
|
||||
|
||||
$clustered = false;
|
||||
if (isset($data->clustered)) {
|
||||
$clustered = true;
|
||||
}
|
||||
|
||||
$lines = explode("\n", $data->setservers);
|
||||
$setservers = array();
|
||||
foreach ($lines as $line) {
|
||||
// Trim surrounding colons and default whitespace.
|
||||
$line = trim(trim($line), ":");
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$setserver = explode(':', $line, 3);
|
||||
// We don't use weights, so display a debug message.
|
||||
if (count($setserver) > 2) {
|
||||
debugging('Memcached Set Server '.$setserver[0].' has too many parameters.');
|
||||
}
|
||||
$setservers[] = $setserver;
|
||||
}
|
||||
|
||||
return array(
|
||||
'servers' => $servers,
|
||||
'compression' => $data->compression,
|
||||
@@ -389,6 +518,8 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
'prefix' => $data->prefix,
|
||||
'hash' => $data->hash,
|
||||
'bufferwrites' => $data->bufferwrites,
|
||||
'clustered' => $clustered,
|
||||
'setservers' => $setservers
|
||||
);
|
||||
}
|
||||
|
||||
@@ -422,6 +553,16 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
if (isset($config['bufferwrites'])) {
|
||||
$data['bufferwrites'] = (bool)$config['bufferwrites'];
|
||||
}
|
||||
if (isset($config['clustered'])) {
|
||||
$data['clustered'] = (bool)$config['clustered'];
|
||||
}
|
||||
if (!empty($config['setservers'])) {
|
||||
$servers = array();
|
||||
foreach ($config['setservers'] as $server) {
|
||||
$servers[] = join(":", $server);
|
||||
}
|
||||
$data['setservers'] = join("\n", $servers);
|
||||
}
|
||||
$editform->set_data($data);
|
||||
}
|
||||
|
||||
@@ -464,7 +605,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
}
|
||||
|
||||
$configuration = array();
|
||||
$configuration['servers'] = $config->testservers;
|
||||
$configuration['servers'] = explode("\n", $config->testservers);
|
||||
if (!empty($config->testcompression)) {
|
||||
$configuration['compression'] = $config->testcompression;
|
||||
}
|
||||
@@ -480,8 +621,19 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
|
||||
if (!empty($config->testbufferwrites)) {
|
||||
$configuration['bufferwrites'] = $config->testbufferwrites;
|
||||
}
|
||||
if (!empty($config->testclustered)) {
|
||||
$configuration['clustered'] = $config->testclustered;
|
||||
}
|
||||
if (!empty($config->testsetservers)) {
|
||||
$configuration['setservers'] = explode("\n", $config->testsetservers);
|
||||
}
|
||||
if (!empty($config->testname)) {
|
||||
$name = $config->testname;
|
||||
} else {
|
||||
$name = 'Test memcached';
|
||||
}
|
||||
|
||||
$store = new cachestore_memcached('Test memcached', $configuration);
|
||||
$store = new cachestore_memcached($name, $configuration);
|
||||
$store->initialise($definition);
|
||||
|
||||
return $store;
|
||||
|
||||
Vendored
+1
-1
@@ -30,4 +30,4 @@ $settings->add(new admin_setting_configtextarea(
|
||||
'cachestore_memcached/testservers',
|
||||
new lang_string('testservers', 'cachestore_memcached'),
|
||||
new lang_string('testservers_desc', 'cachestore_memcached'),
|
||||
'', PARAM_RAW, 60, 3));
|
||||
'', PARAM_RAW, 60, 3));
|
||||
|
||||
+186
@@ -54,6 +54,8 @@ class cachestore_memcached_test extends cachestore_tests {
|
||||
* Tests the valid keys to ensure they work.
|
||||
*/
|
||||
public function test_valid_keys() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
|
||||
$instance = cachestore_memcached::initialise_unit_test_instance($definition);
|
||||
|
||||
@@ -69,15 +71,199 @@ class cachestore_memcached_test extends cachestore_tests {
|
||||
// Underscores.
|
||||
'a_1', '1_a', '_a1', 'a1_'
|
||||
);
|
||||
|
||||
// Set some keys.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
|
||||
}
|
||||
|
||||
// Get some keys.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
|
||||
}
|
||||
|
||||
// Try get many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($values as $key => $value) {
|
||||
$this->assertEquals($key, $value);
|
||||
}
|
||||
|
||||
// Reset a key.
|
||||
$this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
|
||||
$this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
|
||||
$this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
|
||||
}
|
||||
|
||||
// Try set many, and check that count is correct.
|
||||
$many = array();
|
||||
foreach ($keys as $key) {
|
||||
$many[] = array('key' => $key, 'value' => $key);
|
||||
}
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Check keys retrieved with get_many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
|
||||
$this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
|
||||
}
|
||||
|
||||
// Delete many, make sure count matches.
|
||||
$returncount = $instance->delete_many($keys);
|
||||
$this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
|
||||
|
||||
// Check that each key was deleted.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
|
||||
}
|
||||
|
||||
// Set the keys again.
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Purge.
|
||||
$this->assertTrue($instance->purge(), 'Failure to purge');
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the clustering feature.
|
||||
*/
|
||||
public function test_clustered() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
|
||||
|
||||
if (count($testservers) < 2) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
// Use the first server as our primary.
|
||||
// We need to set a prefix for all, otherwise it uses the name, which will not match between connections.
|
||||
set_config('testprefix', 'pre', 'cachestore_memcached');
|
||||
// We need to set a name, otherwise we get a reused connection.
|
||||
set_config('testname', 'cluster', 'cachestore_memcached');
|
||||
set_config('testservers', $testservers[0], 'cachestore_memcached');
|
||||
set_config('testsetservers', TEST_CACHESTORE_MEMCACHED_TESTSERVERS, 'cachestore_memcached');
|
||||
set_config('testclustered', true, 'cachestore_memcached');
|
||||
|
||||
// First and instance that we can use to test the second server.
|
||||
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
|
||||
$instance = cachestore_memcached::initialise_test_instance($definition);
|
||||
|
||||
if (!$instance) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
// Now we are going to setup a connection to each independent server.
|
||||
set_config('testclustered', false, 'cachestore_memcached');
|
||||
set_config('testsetservers', '', 'cachestore_memcached');
|
||||
$checkinstances = array();
|
||||
foreach ($testservers as $testserver) {
|
||||
// We need to set a name, otherwise we get a reused connection.
|
||||
set_config('testname', $testserver, 'cachestore_memcached');
|
||||
set_config('testservers', $testserver, 'cachestore_memcached');
|
||||
$checkinstance = cachestore_memcached::initialise_test_instance($definition);
|
||||
if (!$checkinstance) {
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
$checkinstances[] = $checkinstance;
|
||||
}
|
||||
|
||||
$keys = array(
|
||||
// Alphanumeric.
|
||||
'abc', 'ABC', '123', 'aB1', '1aB',
|
||||
// Hyphens.
|
||||
'a-1', '1-a', '-a1', 'a1-',
|
||||
// Underscores.
|
||||
'a_1', '1_a', '_a1', 'a1_'
|
||||
);
|
||||
|
||||
// Set each key.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
|
||||
}
|
||||
|
||||
// Check each key.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertEquals($key, $checkinstance->get($key), "Failed to get key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Reset a key.
|
||||
$this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
|
||||
$this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertEquals('New', $checkinstance->get($keys[0]), "Failed to get reset key `$key` from server $id");
|
||||
}
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
|
||||
$this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved deleted key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Try set many, and check that count is correct.
|
||||
$many = array();
|
||||
foreach ($keys as $key) {
|
||||
$many[] = array('key' => $key, 'value' => $key);
|
||||
}
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Check keys retrieved with get_many.
|
||||
$values = $instance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
|
||||
$this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
|
||||
}
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$values = $checkinstance->get_many($keys);
|
||||
foreach ($keys as $key) {
|
||||
$this->assertTrue(isset($values[$key]), "Failed to get_many key `$key` from server $id");
|
||||
$this->assertEquals($key, $values[$key], "Failed to get_many key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete many, make sure count matches.
|
||||
$returncount = $instance->delete_many($keys);
|
||||
$this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
|
||||
|
||||
// Check that each key was deleted.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved many deleted key `$key` from server $id");
|
||||
}
|
||||
}
|
||||
|
||||
// Set the keys again.
|
||||
$returncount = $instance->set_many($many);
|
||||
$this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
|
||||
|
||||
// Purge.
|
||||
$this->assertTrue($instance->purge(), 'Failure to purge');
|
||||
|
||||
// Delete and check that we can't retrieve.
|
||||
foreach ($keys as $key) {
|
||||
$this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
|
||||
foreach ($checkinstances as $id => $checkinstance) {
|
||||
$this->assertFalse($checkinstance->get($key), "Retrieved purged key `$key` from server 2");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user