MDL-53779 external: get_string and get_strings use PARAM_RAW
[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 {
47     /**
48      * The name of the store
49      * @var store
50      */
51     protected $name;
53     /**
54      * The memcached connection
55      * @var Memcached
56      */
57     protected $connection;
59     /**
60      * An array of servers to use during connection
61      * @var array
62      */
63     protected $servers = array();
65     /**
66      * The options used when establishing the connection
67      * @var array
68      */
69     protected $options = array();
71     /**
72      * True when this instance is ready to be initialised.
73      * @var bool
74      */
75     protected $isready = false;
77     /**
78      * Set to true when this store instance has been initialised.
79      * @var bool
80      */
81     protected $isinitialised = false;
83     /**
84      * The cache definition this store was initialised with.
85      * @var cache_definition
86      */
87     protected $definition;
89     /**
90      * Set to true when this store is clustered.
91      * @var bool
92      */
93     protected $clustered = false;
95     /**
96      * Array of servers to set when in clustered mode.
97      * @var array
98      */
99     protected $setservers = array();
101     /**
102      * The an array of memcache connections for the set servers, once established.
103      * @var array
104      */
105     protected $setconnections = array();
107     /**
108      * Constructs the store instance.
109      *
110      * Noting that this function is not an initialisation. It is used to prepare the store for use.
111      * The store will be initialised when required and will be provided with a cache_definition at that time.
112      *
113      * @param string $name
114      * @param array $configuration
115      */
116     public function __construct($name, array $configuration = array()) {
117         $this->name = $name;
118         if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
119             // Nothing configured.
120             return;
121         }
122         if (!is_array($configuration['servers'])) {
123             $configuration['servers'] = array($configuration['servers']);
124         }
126         $compression = array_key_exists('compression', $configuration) ? (bool)$configuration['compression'] : true;
127         if (array_key_exists('serialiser', $configuration)) {
128             $serialiser = (int)$configuration['serialiser'];
129         } else {
130             $serialiser = Memcached::SERIALIZER_PHP;
131         }
132         $prefix = (!empty($configuration['prefix'])) ? (string)$configuration['prefix'] : crc32($name);
133         $hashmethod = (array_key_exists('hash', $configuration)) ? (int)$configuration['hash'] : Memcached::HASH_DEFAULT;
134         $bufferwrites = array_key_exists('bufferwrites', $configuration) ? (bool)$configuration['bufferwrites'] : false;
136         foreach ($configuration['servers'] as $server) {
137             if (!is_array($server)) {
138                 $server = explode(':', $server, 3);
139             }
140             if (!array_key_exists(1, $server)) {
141                 $server[1] = 11211;
142                 $server[2] = 100;
143             } else if (!array_key_exists(2, $server)) {
144                 $server[2] = 100;
145             }
146             $this->servers[] = $server;
147         }
149         $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
151         if ($this->clustered) {
152             if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
153                 // Can't setup clustering without set servers.
154                 return;
155             }
156             if (count($this->servers) !== 1) {
157                 // Can only setup cluster with exactly 1 get server.
158                 return;
159             }
160             foreach ($configuration['setservers'] as $server) {
161                 // We do not use weights (3rd part) on these servers.
162                 if (!is_array($server)) {
163                     $server = explode(':', $server, 3);
164                 }
165                 if (!array_key_exists(1, $server)) {
166                     $server[1] = 11211;
167                 }
168                 $this->setservers[] = $server;
169             }
170         }
172         $this->options[Memcached::OPT_COMPRESSION] = $compression;
173         $this->options[Memcached::OPT_SERIALIZER] = $serialiser;
174         $this->options[Memcached::OPT_PREFIX_KEY] = $prefix;
175         $this->options[Memcached::OPT_HASH] = $hashmethod;
176         $this->options[Memcached::OPT_BUFFER_WRITES] = $bufferwrites;
178         $this->connection = new Memcached(crc32($this->name));
179         $servers = $this->connection->getServerList();
180         if (empty($servers)) {
181             foreach ($this->options as $key => $value) {
182                 $this->connection->setOption($key, $value);
183             }
184             $this->connection->addServers($this->servers);
185         }
187         if ($this->clustered) {
188             foreach ($this->setservers as $setserver) {
189                 // Since we will have a number of them with the same name, append server and port.
190                 $connection = new Memcached(crc32($this->name.$setserver[0].$setserver[1]));
191                 foreach ($this->options as $key => $value) {
192                     $connection->setOption($key, $value);
193                 }
194                 $connection->addServer($setserver[0], $setserver[1]);
195                 $this->setconnections[] = $connection;
196             }
197         }
199         // Test the connection to the main connection.
200         $this->isready = @$this->connection->set("ping", 'ping', 1);
201     }
203     /**
204      * Initialises the cache.
205      *
206      * Once this has been done the cache is all set to be used.
207      *
208      * @param cache_definition $definition
209      */
210     public function initialise(cache_definition $definition) {
211         if ($this->is_initialised()) {
212             throw new coding_exception('This memcached instance has already been initialised.');
213         }
214         $this->definition = $definition;
215         $this->isinitialised = true;
216     }
218     /**
219      * Returns true once this instance has been initialised.
220      *
221      * @return bool
222      */
223     public function is_initialised() {
224         return ($this->isinitialised);
225     }
227     /**
228      * Returns true if this store instance is ready to be used.
229      * @return bool
230      */
231     public function is_ready() {
232         return $this->isready;
233     }
235     /**
236      * Returns true if the store requirements are met.
237      *
238      * @return bool
239      */
240     public static function are_requirements_met() {
241         return class_exists('Memcached');
242     }
244     /**
245      * Returns true if the given mode is supported by this store.
246      *
247      * @param int $mode One of cache_store::MODE_*
248      * @return bool
249      */
250     public static function is_supported_mode($mode) {
251         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
252     }
254     /**
255      * Returns the supported features as a combined int.
256      *
257      * @param array $configuration
258      * @return int
259      */
260     public static function get_supported_features(array $configuration = array()) {
261         return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
262     }
264     /**
265      * Returns false as this store does not support multiple identifiers.
266      * (This optional function is a performance optimisation; it must be
267      * consistent with the value from get_supported_features.)
268      *
269      * @return bool False
270      */
271     public function supports_multiple_identifiers() {
272         return false;
273     }
275     /**
276      * Returns the supported modes as a combined int.
277      *
278      * @param array $configuration
279      * @return int
280      */
281     public static function get_supported_modes(array $configuration = array()) {
282         return self::MODE_APPLICATION;
283     }
285     /**
286      * Retrieves an item from the cache store given its key.
287      *
288      * @param string $key The key to retrieve
289      * @return mixed The data that was associated with the key, or false if the key did not exist.
290      */
291     public function get($key) {
292         return $this->connection->get($key);
293     }
295     /**
296      * Retrieves several items from the cache store in a single transaction.
297      *
298      * 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.
299      *
300      * @param array $keys The array of keys to retrieve
301      * @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
302      *      be set to false.
303      */
304     public function get_many($keys) {
305         $return = array();
306         $result = $this->connection->getMulti($keys);
307         if (!is_array($result)) {
308             $result = array();
309         }
310         foreach ($keys as $key) {
311             if (!array_key_exists($key, $result)) {
312                 $return[$key] = false;
313             } else {
314                 $return[$key] = $result[$key];
315             }
316         }
317         return $return;
318     }
320     /**
321      * Sets an item in the cache given its key and data value.
322      *
323      * @param string $key The key to use.
324      * @param mixed $data The data to set.
325      * @return bool True if the operation was a success false otherwise.
326      */
327     public function set($key, $data) {
328         if ($this->clustered) {
329             $status = true;
330             foreach ($this->setconnections as $connection) {
331                 $status = $connection->set($key, $data, $this->definition->get_ttl()) && $status;
332             }
333             return $status;
334         }
336         return $this->connection->set($key, $data, $this->definition->get_ttl());
337     }
339     /**
340      * Sets many items in the cache in a single transaction.
341      *
342      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
343      *      keys, 'key' and 'value'.
344      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
345      *      sent ... if they care that is.
346      */
347     public function set_many(array $keyvaluearray) {
348         $pairs = array();
349         foreach ($keyvaluearray as $pair) {
350             $pairs[$pair['key']] = $pair['value'];
351         }
353         $status = true;
354         if ($this->clustered) {
355             foreach ($this->setconnections as $connection) {
356                 $status = $connection->setMulti($pairs, $this->definition->get_ttl()) && $status;
357             }
358         } else {
359             $status = $this->connection->setMulti($pairs, $this->definition->get_ttl());
360         }
362         if ($status) {
363             return count($keyvaluearray);
364         }
365         return 0;
366     }
368     /**
369      * Deletes an item from the cache store.
370      *
371      * @param string $key The key to delete.
372      * @return bool Returns true if the operation was a success, false otherwise.
373      */
374     public function delete($key) {
375         if ($this->clustered) {
376             $status = true;
377             foreach ($this->setconnections as $connection) {
378                 $status = $connection->delete($key) && $status;
379             }
380             return $status;
381         }
383         return $this->connection->delete($key);
384     }
386     /**
387      * Deletes several keys from the cache in a single action.
388      *
389      * @param array $keys The keys to delete
390      * @return int The number of items successfully deleted.
391      */
392     public function delete_many(array $keys) {
393         if ($this->clustered) {
394             // Get the minimum deleted from any of the connections.
395             $count = count($keys);
396             foreach ($this->setconnections as $connection) {
397                 $count = min($this->delete_many_connection($connection, $keys), $count);
398             }
399             return $count;
400         }
402         return $this->delete_many_connection($this->connection, $keys);
403     }
405     /**
406      * Deletes several keys from the cache in a single action for a specific connection.
407      *
408      * @param Memcached $connection The connection to work on.
409      * @param array $keys The keys to delete
410      * @return int The number of items successfully deleted.
411      */
412     protected function delete_many_connection(Memcached $connection, array $keys) {
413         $count = 0;
414         foreach ($keys as $key) {
415             if ($connection->delete($key)) {
416                 $count++;
417             }
418         }
419         return $count;
420     }
422     /**
423      * Purges the cache deleting all items within it.
424      *
425      * @return boolean True on success. False otherwise.
426      */
427     public function purge() {
428         if ($this->isready) {
429             if ($this->clustered) {
430                 foreach ($this->setconnections as $connection) {
431                     $connection->flush();
432                 }
433             } else {
434                 $this->connection->flush();
435             }
436         }
438         return true;
439     }
441     /**
442      * Gets an array of options to use as the serialiser.
443      * @return array
444      */
445     public static function config_get_serialiser_options() {
446         $options = array(
447             Memcached::SERIALIZER_PHP => get_string('serialiser_php', 'cachestore_memcached')
448         );
449         if (Memcached::HAVE_JSON) {
450             $options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached');
451         }
452         if (Memcached::HAVE_IGBINARY) {
453             $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached');
454         }
455         return $options;
456     }
458     /**
459      * Gets an array of hash options available during configuration.
460      * @return array
461      */
462     public static function config_get_hash_options() {
463         $options = array(
464             Memcached::HASH_DEFAULT => get_string('hash_default', 'cachestore_memcached'),
465             Memcached::HASH_MD5 => get_string('hash_md5', 'cachestore_memcached'),
466             Memcached::HASH_CRC => get_string('hash_crc', 'cachestore_memcached'),
467             Memcached::HASH_FNV1_64 => get_string('hash_fnv1_64', 'cachestore_memcached'),
468             Memcached::HASH_FNV1A_64 => get_string('hash_fnv1a_64', 'cachestore_memcached'),
469             Memcached::HASH_FNV1_32 => get_string('hash_fnv1_32', 'cachestore_memcached'),
470             Memcached::HASH_FNV1A_32 => get_string('hash_fnv1a_32', 'cachestore_memcached'),
471             Memcached::HASH_HSIEH => get_string('hash_hsieh', 'cachestore_memcached'),
472             Memcached::HASH_MURMUR => get_string('hash_murmur', 'cachestore_memcached'),
473         );
474         return $options;
475     }
477     /**
478      * Given the data from the add instance form this function creates a configuration array.
479      *
480      * @param stdClass $data
481      * @return array
482      */
483     public static function config_get_configuration_array($data) {
484         $lines = explode("\n", $data->servers);
485         $servers = array();
486         foreach ($lines as $line) {
487             // Trim surrounding colons and default whitespace.
488             $line = trim(trim($line), ":");
489             // Skip blank lines.
490             if ($line === '') {
491                 continue;
492             }
493             $servers[] = explode(':', $line, 3);
494         }
496         $clustered = false;
497         $setservers = array();
498         if (isset($data->clustered)) {
499             $clustered = true;
501             $lines = explode("\n", $data->setservers);
502             foreach ($lines as $line) {
503                 // Trim surrounding colons and default whitespace.
504                 $line = trim(trim($line), ":");
505                 if ($line === '') {
506                     continue;
507                 }
508                 $setserver = explode(':', $line, 3);
509                 // We don't use weights, so display a debug message.
510                 if (count($setserver) > 2) {
511                     debugging('Memcached Set Server '.$setserver[0].' has too many parameters.');
512                 }
513                 $setservers[] = $setserver;
514             }
515         }
517         return array(
518             'servers' => $servers,
519             'compression' => $data->compression,
520             'serialiser' => $data->serialiser,
521             'prefix' => $data->prefix,
522             'hash' => $data->hash,
523             'bufferwrites' => $data->bufferwrites,
524             'clustered' => $clustered,
525             'setservers' => $setservers
526         );
527     }
529     /**
530      * Allows the cache store to set its data against the edit form before it is shown to the user.
531      *
532      * @param moodleform $editform
533      * @param array $config
534      */
535     public static function config_set_edit_form_data(moodleform $editform, array $config) {
536         $data = array();
537         if (!empty($config['servers'])) {
538             $servers = array();
539             foreach ($config['servers'] as $server) {
540                 $servers[] = join(":", $server);
541             }
542             $data['servers'] = join("\n", $servers);
543         }
544         if (isset($config['compression'])) {
545             $data['compression'] = (bool)$config['compression'];
546         }
547         if (!empty($config['serialiser'])) {
548             $data['serialiser'] = $config['serialiser'];
549         }
550         if (!empty($config['prefix'])) {
551             $data['prefix'] = $config['prefix'];
552         }
553         if (!empty($config['hash'])) {
554             $data['hash'] = $config['hash'];
555         }
556         if (isset($config['bufferwrites'])) {
557             $data['bufferwrites'] = (bool)$config['bufferwrites'];
558         }
559         if (isset($config['clustered'])) {
560             $data['clustered'] = (bool)$config['clustered'];
561         }
562         if (!empty($config['setservers'])) {
563             $servers = array();
564             foreach ($config['setservers'] as $server) {
565                 $servers[] = join(":", $server);
566             }
567             $data['setservers'] = join("\n", $servers);
568         }
569         $editform->set_data($data);
570     }
572     /**
573      * Performs any necessary clean up when the store instance is being deleted.
574      */
575     public function instance_deleted() {
576         if ($this->connection) {
577             $connection = $this->connection;
578         } else {
579             $connection = new Memcached(crc32($this->name));
580             $servers = $connection->getServerList();
581             if (empty($servers)) {
582                 foreach ($this->options as $key => $value) {
583                     $connection->setOption($key, $value);
584                 }
585                 $connection->addServers($this->servers);
586             }
587         }
588         @$connection->flush();
589         unset($connection);
590         unset($this->connection);
591     }
593     /**
594      * Generates an instance of the cache store that can be used for testing.
595      *
596      * @param cache_definition $definition
597      * @return cachestore_memcached|false
598      */
599     public static function initialise_test_instance(cache_definition $definition) {
601         if (!self::are_requirements_met()) {
602             return false;
603         }
605         $config = get_config('cachestore_memcached');
606         if (empty($config->testservers)) {
607             return false;
608         }
610         $configuration = array();
611         $configuration['servers'] = explode("\n", $config->testservers);
612         if (!empty($config->testcompression)) {
613             $configuration['compression'] = $config->testcompression;
614         }
615         if (!empty($config->testserialiser)) {
616             $configuration['serialiser'] = $config->testserialiser;
617         }
618         if (!empty($config->testprefix)) {
619             $configuration['prefix'] = $config->testprefix;
620         }
621         if (!empty($config->testhash)) {
622             $configuration['hash'] = $config->testhash;
623         }
624         if (!empty($config->testbufferwrites)) {
625             $configuration['bufferwrites'] = $config->testbufferwrites;
626         }
627         if (!empty($config->testclustered)) {
628             $configuration['clustered'] = $config->testclustered;
629         }
630         if (!empty($config->testsetservers)) {
631             $configuration['setservers'] = explode("\n", $config->testsetservers);
632         }
633         if (!empty($config->testname)) {
634             $name = $config->testname;
635         } else {
636             $name = 'Test memcached';
637         }
639         $store = new cachestore_memcached($name, $configuration);
640         $store->initialise($definition);
642         return $store;
643     }
645     /**
646      * Creates a test instance for unit tests if possible.
647      * @param cache_definition $definition
648      * @return bool|cachestore_memcached
649      */
650     public static function initialise_unit_test_instance(cache_definition $definition) {
651         if (!self::are_requirements_met()) {
652             return false;
653         }
654         if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
655             return false;
656         }
658         $configuration = array();
659         $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS);
661         $store = new cachestore_memcached('Test memcached', $configuration);
662         $store->initialise($definition);
664         return $store;
665     }
667     /**
668      * Returns the name of this instance.
669      * @return string
670      */
671     public function my_name() {
672         return $this->name;
673     }
675     /**
676      * Used to notify of configuration conflicts.
677      *
678      * The warnings returned here will be displayed on the cache configuration screen.
679      *
680      * @return string[] Returns an array of warnings (strings)
681      */
682     public function get_warnings() {
683         global $CFG;
684         $warnings = array();
685         if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
686             $bits = explode(':', $CFG->session_memcached_save_path, 3);
687             $host = array_shift($bits);
688             $port = (count($bits)) ? array_shift($bits) : '11211';
690             foreach ($this->servers as $server) {
691                 if ((string)$server[0] === $host && (string)$server[1] === $port) {
692                     $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcached', $this->my_name());
693                     break;
694                 }
695             }
696         }
697         return $warnings;
698     }
700     /**
701      * Returns true if this cache store instance is both suitable for testing, and ready for testing.
702      *
703      * Cache stores that support being used as the default store for unit and acceptance testing should
704      * override this function and return true if there requirements have been met.
705      *
706      * @return bool
707      */
708     public static function ready_to_be_used_for_testing() {
709         return defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS');
710     }