Merge branch 'MDL-53779-master' of git://github.com/FMCorz/moodle
[moodle.git] / cache / stores / memcached / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * The library file for the memcached cache store.
19  *
20  * This file is part of the memcached cache store, it contains the API for interacting with an instance of the store.
21  *
22  * @package    cachestore_memcached
23  * @copyright  2012 Sam Hemelryk
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /**
30  * The memcached store.
31  *
32  * (Not to be confused with the memcache store)
33  *
34  * Configuration options:
35  *      servers:        string: host:port:weight , ...
36  *      compression:    true, false
37  *      serialiser:     SERIALIZER_PHP, SERIALIZER_JSON, SERIALIZER_IGBINARY
38  *      prefix:         string: defaults to instance name
39  *      hashmethod:     HASH_DEFAULT, HASH_MD5, HASH_CRC, HASH_FNV1_64, HASH_FNV1A_64, HASH_FNV1_32,
40  *                      HASH_FNV1A_32, HASH_HSIEH, HASH_MURMUR
41  *      bufferwrites:   true, false
42  *
43  * @copyright  2012 Sam Hemelryk
44  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45  */
46 class cachestore_memcached extends cache_store implements cache_is_configurable {
48     /**
49      * The minimum required version of memcached in order to use this store.
50      */
51     const REQUIRED_VERSION = '2.0.0';
53     /**
54      * The name of the store
55      * @var store
56      */
57     protected $name;
59     /**
60      * The memcached connection
61      * @var Memcached
62      */
63     protected $connection;
65     /**
66      * An array of servers to use during connection
67      * @var array
68      */
69     protected $servers = array();
71     /**
72      * The options used when establishing the connection
73      * @var array
74      */
75     protected $options = array();
77     /**
78      * True when this instance is ready to be initialised.
79      * @var bool
80      */
81     protected $isready = false;
83     /**
84      * Set to true when this store instance has been initialised.
85      * @var bool
86      */
87     protected $isinitialised = false;
89     /**
90      * The cache definition this store was initialised with.
91      * @var cache_definition
92      */
93     protected $definition;
95     /**
96      * Set to true when this store is clustered.
97      * @var bool
98      */
99     protected $clustered = false;
101     /**
102      * Array of servers to set when in clustered mode.
103      * @var array
104      */
105     protected $setservers = array();
107     /**
108      * The an array of memcache connections for the set servers, once established.
109      * @var array
110      */
111     protected $setconnections = array();
113     /**
114      * The prefix to use on all keys.
115      * @var string
116      */
117     protected $prefix = '';
119     /**
120      * True if Memcached::deleteMulti can be used, false otherwise.
121      * This required extension version 2.0.0 or greater.
122      * @var bool
123      */
124     protected $candeletemulti = false;
126     /**
127      * True if the memcached server is shared, false otherwise.
128      * This required extension version 2.0.0 or greater.
129      * @var bool
130      */
131     protected $isshared = false;
133     /**
134      * Constructs the store instance.
135      *
136      * Noting that this function is not an initialisation. It is used to prepare the store for use.
137      * The store will be initialised when required and will be provided with a cache_definition at that time.
138      *
139      * @param string $name
140      * @param array $configuration
141      */
142     public function __construct($name, array $configuration = array()) {
143         $this->name = $name;
144         if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
145             // Nothing configured.
146             return;
147         }
148         if (!is_array($configuration['servers'])) {
149             $configuration['servers'] = array($configuration['servers']);
150         }
152         $compression = array_key_exists('compression', $configuration) ? (bool)$configuration['compression'] : true;
153         if (array_key_exists('serialiser', $configuration)) {
154             $serialiser = (int)$configuration['serialiser'];
155         } else {
156             $serialiser = Memcached::SERIALIZER_PHP;
157         }
158         $prefix = (!empty($configuration['prefix'])) ? (string)$configuration['prefix'] : crc32($name);
159         $hashmethod = (array_key_exists('hash', $configuration)) ? (int)$configuration['hash'] : Memcached::HASH_DEFAULT;
160         $bufferwrites = array_key_exists('bufferwrites', $configuration) ? (bool)$configuration['bufferwrites'] : false;
162         foreach ($configuration['servers'] as $server) {
163             if (!is_array($server)) {
164                 $server = explode(':', $server, 3);
165             }
166             if (!array_key_exists(1, $server)) {
167                 $server[1] = 11211;
168                 $server[2] = 100;
169             } else if (!array_key_exists(2, $server)) {
170                 $server[2] = 100;
171             }
172             $this->servers[] = $server;
173         }
175         $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
177         if ($this->clustered) {
178             if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
179                 // Can't setup clustering without set servers.
180                 return;
181             }
182             if (count($this->servers) !== 1) {
183                 // Can only setup cluster with exactly 1 get server.
184                 return;
185             }
186             foreach ($configuration['setservers'] as $server) {
187                 // We do not use weights (3rd part) on these servers.
188                 if (!is_array($server)) {
189                     $server = explode(':', $server, 3);
190                 }
191                 if (!array_key_exists(1, $server)) {
192                     $server[1] = 11211;
193                 }
194                 $this->setservers[] = $server;
195             }
196         }
198         $this->options[Memcached::OPT_COMPRESSION] = $compression;
199         $this->options[Memcached::OPT_SERIALIZER] = $serialiser;
200         $this->options[Memcached::OPT_PREFIX_KEY] = $this->prefix = (string)$prefix;
201         $this->options[Memcached::OPT_HASH] = $hashmethod;
202         $this->options[Memcached::OPT_BUFFER_WRITES] = $bufferwrites;
204         $this->connection = new Memcached(crc32($this->name));
205         $servers = $this->connection->getServerList();
206         if (empty($servers)) {
207             foreach ($this->options as $key => $value) {
208                 $this->connection->setOption($key, $value);
209             }
210             $this->connection->addServers($this->servers);
211         }
213         if ($this->clustered) {
214             foreach ($this->setservers as $setserver) {
215                 // Since we will have a number of them with the same name, append server and port.
216                 $connection = new Memcached(crc32($this->name.$setserver[0].$setserver[1]));
217                 foreach ($this->options as $key => $value) {
218                     $connection->setOption($key, $value);
219                 }
220                 $connection->addServer($setserver[0], $setserver[1]);
221                 $this->setconnections[] = $connection;
222             }
223         }
225         if (isset($configuration['isshared'])) {
226             $this->isshared = $configuration['isshared'];
227         }
229         $version = phpversion('memcached');
230         $this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
232         // Test the connection to the main connection.
233         $this->isready = @$this->connection->set("ping", 'ping', 1);
234     }
236     /**
237      * Initialises the cache.
238      *
239      * Once this has been done the cache is all set to be used.
240      *
241      * @throws coding_exception if the instance has already been initialised.
242      * @param cache_definition $definition
243      */
244     public function initialise(cache_definition $definition) {
245         if ($this->is_initialised()) {
246             throw new coding_exception('This memcached instance has already been initialised.');
247         }
248         $this->definition = $definition;
249         $this->isinitialised = true;
250     }
252     /**
253      * Returns true once this instance has been initialised.
254      *
255      * @return bool
256      */
257     public function is_initialised() {
258         return ($this->isinitialised);
259     }
261     /**
262      * Returns true if this store instance is ready to be used.
263      * @return bool
264      */
265     public function is_ready() {
266         return $this->isready;
267     }
269     /**
270      * Returns true if the store requirements are met.
271      *
272      * @return bool
273      */
274     public static function are_requirements_met() {
275         return extension_loaded('memcached') && class_exists('Memcached');
276     }
278     /**
279      * Returns true if the given mode is supported by this store.
280      *
281      * @param int $mode One of cache_store::MODE_*
282      * @return bool
283      */
284     public static function is_supported_mode($mode) {
285         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
286     }
288     /**
289      * Returns the supported features as a combined int.
290      *
291      * @param array $configuration
292      * @return int
293      */
294     public static function get_supported_features(array $configuration = array()) {
295         return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
296     }
298     /**
299      * Returns false as this store does not support multiple identifiers.
300      * (This optional function is a performance optimisation; it must be
301      * consistent with the value from get_supported_features.)
302      *
303      * @return bool False
304      */
305     public function supports_multiple_identifiers() {
306         return false;
307     }
309     /**
310      * Returns the supported modes as a combined int.
311      *
312      * @param array $configuration
313      * @return int
314      */
315     public static function get_supported_modes(array $configuration = array()) {
316         return self::MODE_APPLICATION;
317     }
319     /**
320      * Retrieves an item from the cache store given its key.
321      *
322      * @param string $key The key to retrieve
323      * @return mixed The data that was associated with the key, or false if the key did not exist.
324      */
325     public function get($key) {
326         return $this->connection->get($key);
327     }
329     /**
330      * Retrieves several items from the cache store in a single transaction.
331      *
332      * 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.
333      *
334      * @param array $keys The array of keys to retrieve
335      * @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
336      *      be set to false.
337      */
338     public function get_many($keys) {
339         $return = array();
340         $result = $this->connection->getMulti($keys);
341         if (!is_array($result)) {
342             $result = array();
343         }
344         foreach ($keys as $key) {
345             if (!array_key_exists($key, $result)) {
346                 $return[$key] = false;
347             } else {
348                 $return[$key] = $result[$key];
349             }
350         }
351         return $return;
352     }
354     /**
355      * Sets an item in the cache given its key and data value.
356      *
357      * @param string $key The key to use.
358      * @param mixed $data The data to set.
359      * @return bool True if the operation was a success false otherwise.
360      */
361     public function set($key, $data) {
362         if ($this->clustered) {
363             $status = true;
364             foreach ($this->setconnections as $connection) {
365                 $status = $connection->set($key, $data, $this->definition->get_ttl()) && $status;
366             }
367             return $status;
368         }
370         return $this->connection->set($key, $data, $this->definition->get_ttl());
371     }
373     /**
374      * Sets many items in the cache in a single transaction.
375      *
376      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
377      *      keys, 'key' and 'value'.
378      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
379      *      sent ... if they care that is.
380      */
381     public function set_many(array $keyvaluearray) {
382         $pairs = array();
383         foreach ($keyvaluearray as $pair) {
384             $pairs[$pair['key']] = $pair['value'];
385         }
387         $status = true;
388         if ($this->clustered) {
389             foreach ($this->setconnections as $connection) {
390                 $status = $connection->setMulti($pairs, $this->definition->get_ttl()) && $status;
391             }
392         } else {
393             $status = $this->connection->setMulti($pairs, $this->definition->get_ttl());
394         }
396         if ($status) {
397             return count($keyvaluearray);
398         }
399         return 0;
400     }
402     /**
403      * Deletes an item from the cache store.
404      *
405      * @param string $key The key to delete.
406      * @return bool Returns true if the operation was a success, false otherwise.
407      */
408     public function delete($key) {
409         if ($this->clustered) {
410             $status = true;
411             foreach ($this->setconnections as $connection) {
412                 $status = $connection->delete($key) && $status;
413             }
414             return $status;
415         }
417         return $this->connection->delete($key);
418     }
420     /**
421      * Deletes several keys from the cache in a single action.
422      *
423      * @param array $keys The keys to delete
424      * @return int The number of items successfully deleted.
425      */
426     public function delete_many(array $keys) {
427         if ($this->clustered) {
428             // Get the minimum deleted from any of the connections.
429             $count = count($keys);
430             foreach ($this->setconnections as $connection) {
431                 $count = min($this->delete_many_connection($connection, $keys), $count);
432             }
433             return $count;
434         }
436         return $this->delete_many_connection($this->connection, $keys);
437     }
439     /**
440      * Deletes several keys from the cache in a single action for a specific connection.
441      *
442      * @param Memcached $connection The connection to work on.
443      * @param array $keys The keys to delete
444      * @return int The number of items successfully deleted.
445      */
446     protected function delete_many_connection(Memcached $connection, array $keys) {
447         $count = 0;
448         if ($this->candeletemulti) {
449             // We can use deleteMulti, this is a bit faster yay!
450             $result = $connection->deleteMulti($keys);
451             foreach ($result as $key => $outcome) {
452                 if ($outcome === true) {
453                     $count++;
454                 }
455             }
456             return $count;
457         }
459         // They are running an older version of the php memcached extension.
460         foreach ($keys as $key) {
461             if ($connection->delete($key)) {
462                 $count++;
463             }
464         }
465         return $count;
466     }
468     /**
469      * Purges the cache deleting all items within it.
470      *
471      * @return boolean True on success. False otherwise.
472      */
473     public function purge() {
474         if ($this->isready) {
475             // Only use delete multi if we have the correct extension installed and if the memcached
476             // server is shared (flushing the cache is quicker otherwise).
477             $candeletemulti = ($this->candeletemulti && $this->isshared);
479             if ($this->clustered) {
480                 foreach ($this->setconnections as $connection) {
481                     if ($candeletemulti) {
482                         $keys = self::get_prefixed_keys($connection, $this->prefix);
483                         $connection->deleteMulti($keys);
484                     } else {
485                         // Oh damn, this isn't multi-site safe.
486                         $connection->flush();
487                     }
488                 }
489             } else if ($candeletemulti) {
490                 $keys = self::get_prefixed_keys($this->connection, $this->prefix);
491                 $this->connection->deleteMulti($keys);
492             } else {
493                 // Oh damn, this isn't multi-site safe.
494                 $this->connection->flush();
495             }
496         }
497         // It never fails. Ever.
498         return true;
499     }
501     /**
502      * Returns all of the keys in the given connection that belong to this cache store instance.
503      *
504      * Requires php memcached extension version 2.0.0 or greater.
505      *
506      * @param Memcached $connection
507      * @param string $prefix
508      * @return array
509      */
510     protected static function get_prefixed_keys(Memcached $connection, $prefix) {
511         $keys = array();
512         $start = strlen($prefix);
513         foreach ($connection->getAllKeys() as $key) {
514             if (strpos($key, $prefix) === 0) {
515                 $keys[] = substr($key, $start);
516             }
517         }
518         return $keys;
519     }
521     /**
522      * Gets an array of options to use as the serialiser.
523      * @return array
524      */
525     public static function config_get_serialiser_options() {
526         $options = array(
527             Memcached::SERIALIZER_PHP => get_string('serialiser_php', 'cachestore_memcached')
528         );
529         if (Memcached::HAVE_JSON) {
530             $options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached');
531         }
532         if (Memcached::HAVE_IGBINARY) {
533             $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached');
534         }
535         return $options;
536     }
538     /**
539      * Gets an array of hash options available during configuration.
540      * @return array
541      */
542     public static function config_get_hash_options() {
543         $options = array(
544             Memcached::HASH_DEFAULT => get_string('hash_default', 'cachestore_memcached'),
545             Memcached::HASH_MD5 => get_string('hash_md5', 'cachestore_memcached'),
546             Memcached::HASH_CRC => get_string('hash_crc', 'cachestore_memcached'),
547             Memcached::HASH_FNV1_64 => get_string('hash_fnv1_64', 'cachestore_memcached'),
548             Memcached::HASH_FNV1A_64 => get_string('hash_fnv1a_64', 'cachestore_memcached'),
549             Memcached::HASH_FNV1_32 => get_string('hash_fnv1_32', 'cachestore_memcached'),
550             Memcached::HASH_FNV1A_32 => get_string('hash_fnv1a_32', 'cachestore_memcached'),
551             Memcached::HASH_HSIEH => get_string('hash_hsieh', 'cachestore_memcached'),
552             Memcached::HASH_MURMUR => get_string('hash_murmur', 'cachestore_memcached'),
553         );
554         return $options;
555     }
557     /**
558      * Given the data from the add instance form this function creates a configuration array.
559      *
560      * @param stdClass $data
561      * @return array
562      */
563     public static function config_get_configuration_array($data) {
564         $lines = explode("\n", $data->servers);
565         $servers = array();
566         foreach ($lines as $line) {
567             // Trim surrounding colons and default whitespace.
568             $line = trim(trim($line), ":");
569             // Skip blank lines.
570             if ($line === '') {
571                 continue;
572             }
573             $servers[] = explode(':', $line, 3);
574         }
576         $clustered = false;
577         $setservers = array();
578         if (isset($data->clustered)) {
579             $clustered = true;
581             $lines = explode("\n", $data->setservers);
582             foreach ($lines as $line) {
583                 // Trim surrounding colons and default whitespace.
584                 $line = trim(trim($line), ":");
585                 if ($line === '') {
586                     continue;
587                 }
588                 $setserver = explode(':', $line, 3);
589                 // We don't use weights, so display a debug message.
590                 if (count($setserver) > 2) {
591                     debugging('Memcached Set Server '.$setserver[0].' has too many parameters.');
592                 }
593                 $setservers[] = $setserver;
594             }
595         }
597         $isshared = false;
598         if (isset($data->isshared)) {
599             $isshared = $data->isshared;
600         }
602         return array(
603             'servers' => $servers,
604             'compression' => $data->compression,
605             'serialiser' => $data->serialiser,
606             'prefix' => $data->prefix,
607             'hash' => $data->hash,
608             'bufferwrites' => $data->bufferwrites,
609             'clustered' => $clustered,
610             'setservers' => $setservers,
611             'isshared' => $isshared
612         );
613     }
615     /**
616      * Allows the cache store to set its data against the edit form before it is shown to the user.
617      *
618      * @param moodleform $editform
619      * @param array $config
620      */
621     public static function config_set_edit_form_data(moodleform $editform, array $config) {
622         $data = array();
623         if (!empty($config['servers'])) {
624             $servers = array();
625             foreach ($config['servers'] as $server) {
626                 $servers[] = join(":", $server);
627             }
628             $data['servers'] = join("\n", $servers);
629         }
630         if (isset($config['compression'])) {
631             $data['compression'] = (bool)$config['compression'];
632         }
633         if (!empty($config['serialiser'])) {
634             $data['serialiser'] = $config['serialiser'];
635         }
636         if (!empty($config['prefix'])) {
637             $data['prefix'] = $config['prefix'];
638         }
639         if (!empty($config['hash'])) {
640             $data['hash'] = $config['hash'];
641         }
642         if (isset($config['bufferwrites'])) {
643             $data['bufferwrites'] = (bool)$config['bufferwrites'];
644         }
645         if (isset($config['clustered'])) {
646             $data['clustered'] = (bool)$config['clustered'];
647         }
648         if (!empty($config['setservers'])) {
649             $servers = array();
650             foreach ($config['setservers'] as $server) {
651                 $servers[] = join(":", $server);
652             }
653             $data['setservers'] = join("\n", $servers);
654         }
655         if (isset($config['isshared'])) {
656             $data['isshared'] = $config['isshared'];
657         }
658         $editform->set_data($data);
659     }
661     /**
662      * Performs any necessary clean up when the store instance is being deleted.
663      */
664     public function instance_deleted() {
665         if ($this->connection) {
666             $connection = $this->connection;
667         } else {
668             $connection = new Memcached(crc32($this->name));
669             $servers = $connection->getServerList();
670             if (empty($servers)) {
671                 foreach ($this->options as $key => $value) {
672                     $connection->setOption($key, $value);
673                 }
674                 $connection->addServers($this->servers);
675             }
676         }
677         // We have to flush here to be sure we are completely cleaned up.
678         // Bad for performance but this is incredibly rare.
679         @$connection->flush();
680         unset($connection);
681         unset($this->connection);
682     }
684     /**
685      * Generates an instance of the cache store that can be used for testing.
686      *
687      * @param cache_definition $definition
688      * @return cachestore_memcached|false
689      */
690     public static function initialise_test_instance(cache_definition $definition) {
692         if (!self::are_requirements_met()) {
693             return false;
694         }
696         $config = get_config('cachestore_memcached');
697         if (empty($config->testservers)) {
698             return false;
699         }
701         $configuration = array();
702         $configuration['servers'] = explode("\n", $config->testservers);
703         if (!empty($config->testcompression)) {
704             $configuration['compression'] = $config->testcompression;
705         }
706         if (!empty($config->testserialiser)) {
707             $configuration['serialiser'] = $config->testserialiser;
708         }
709         if (!empty($config->testprefix)) {
710             $configuration['prefix'] = $config->testprefix;
711         }
712         if (!empty($config->testhash)) {
713             $configuration['hash'] = $config->testhash;
714         }
715         if (!empty($config->testbufferwrites)) {
716             $configuration['bufferwrites'] = $config->testbufferwrites;
717         }
718         if (!empty($config->testclustered)) {
719             $configuration['clustered'] = $config->testclustered;
720         }
721         if (!empty($config->testsetservers)) {
722             $configuration['setservers'] = explode("\n", $config->testsetservers);
723         }
724         if (!empty($config->testname)) {
725             $name = $config->testname;
726         } else {
727             $name = 'Test memcached';
728         }
730         $store = new cachestore_memcached($name, $configuration);
731         $store->initialise($definition);
733         return $store;
734     }
736     /**
737      * Creates a test instance for unit tests if possible.
738      * @param cache_definition $definition
739      * @return bool|cachestore_memcached
740      */
741     public static function initialise_unit_test_instance(cache_definition $definition) {
742         if (!self::are_requirements_met()) {
743             return false;
744         }
745         if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
746             return false;
747         }
749         $configuration = array();
750         $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);
752         $store = new cachestore_memcached('Test memcached', $configuration);
753         $store->initialise($definition);
755         return $store;
756     }
758     /**
759      * Returns the name of this instance.
760      * @return string
761      */
762     public function my_name() {
763         return $this->name;
764     }
766     /**
767      * Used to notify of configuration conflicts.
768      *
769      * The warnings returned here will be displayed on the cache configuration screen.
770      *
771      * @return string[] Returns an array of warnings (strings)
772      */
773     public function get_warnings() {
774         global $CFG;
775         $warnings = array();
776         if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
777             $bits = explode(':', $CFG->session_memcached_save_path, 3);
778             $host = array_shift($bits);
779             $port = (count($bits)) ? array_shift($bits) : '11211';
781             foreach ($this->servers as $server) {
782                 if ((string)$server[0] === $host && (string)$server[1] === $port) {
783                     $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcached', $this->my_name());
784                     break;
785                 }
786             }
787         }
788         return $warnings;
789     }
791     /**
792      * Returns true if this cache store instance is both suitable for testing, and ready for testing.
793      *
794      * Cache stores that support being used as the default store for unit and acceptance testing should
795      * override this function and return true if there requirements have been met.
796      *
797      * @return bool
798      */
799     public static function ready_to_be_used_for_testing() {
800         return defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS');
801     }