036da56566840f2c2e2132e118c9640421df2a3e
[moodle.git] / cache / stores / memcache / 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 memcache cache store.
19  *
20  * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
21  *
22  * @package    cachestore_memcache
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 memcache store class.
31  *
32  * (Not to be confused with memcached store)
33  *
34  * Configuration options:
35  *      servers:        string: host:port:weight , ...
36  *
37  * @copyright  2012 Sam Hemelryk
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class cachestore_memcache extends cache_store implements cache_is_configurable {
42     /**
43      * The name of the store
44      * @var store
45      */
46     protected $name;
48     /**
49      * The memcache connection once established.
50      * @var Memcache
51      */
52     protected $connection;
54     /**
55      * Key prefix for this memcache.
56      * @var string
57      */
58     protected $prefix;
60     /**
61      * An array of servers to use in the connection args.
62      * @var array
63      */
64     protected $servers = array();
66     /**
67      * An array of options used when establishing the connection.
68      * @var array
69      */
70     protected $options = array();
72     /**
73      * Set to true when things are ready to be initialised.
74      * @var bool
75      */
76     protected $isready = false;
78     /**
79      * Set to true once this store instance has been initialised.
80      * @var bool
81      */
82     protected $isinitialised = false;
84     /**
85      * The cache definition this store was initialised for.
86      * @var cache_definition
87      */
88     protected $definition;
90     /**
91      * Set to true when this store is clustered.
92      * @var bool
93      */
94     protected $clustered = false;
96     /**
97      * Array of servers to set when in clustered mode.
98      * @var array
99      */
100     protected $setservers = array();
102     /**
103      * The an array of memcache connections for the set servers, once established.
104      * @var array
105      */
106     protected $setconnections = array();
108     /**
109      * Default prefix for key names.
110      * @var string
111      */
112     const DEFAULT_PREFIX = 'mdl_';
114     /**
115      * Constructs the store instance.
116      *
117      * Noting that this function is not an initialisation. It is used to prepare the store for use.
118      * The store will be initialised when required and will be provided with a cache_definition at that time.
119      *
120      * @param string $name
121      * @param array $configuration
122      */
123     public function __construct($name, array $configuration = array()) {
124         $this->name = $name;
125         if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
126             // Nothing configured.
127             return;
128         }
129         if (!is_array($configuration['servers'])) {
130             $configuration['servers'] = array($configuration['servers']);
131         }
132         foreach ($configuration['servers'] as $server) {
133             if (!is_array($server)) {
134                 $server = explode(':', $server, 3);
135             }
136             if (!array_key_exists(1, $server)) {
137                 $server[1] = 11211;
138                 $server[2] = 100;
139             } else if (!array_key_exists(2, $server)) {
140                 $server[2] = 100;
141             }
142             $this->servers[] = $server;
143         }
145         $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
147         if ($this->clustered) {
148             if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
149                 // Can't setup clustering without set servers.
150                 return;
151             }
152             if (count($this->servers) !== 1) {
153                 // Can only setup cluster with exactly 1 get server.
154                 return;
155             }
156             foreach ($configuration['setservers'] as $server) {
157                 // We do not use weights (3rd part) on these servers.
158                 if (!is_array($server)) {
159                     $server = explode(':', $server, 3);
160                 }
161                 if (!array_key_exists(1, $server)) {
162                     $server[1] = 11211;
163                 }
164                 $this->setservers[] = $server;
165             }
166         }
168         if (empty($configuration['prefix'])) {
169             $this->prefix = self::DEFAULT_PREFIX;
170         } else {
171             $this->prefix = $configuration['prefix'];
172         }
174         $this->connection = new Memcache;
175         foreach ($this->servers as $server) {
176             $this->connection->addServer($server[0], (int) $server[1], true, (int) $server[2]);
177         }
179         if ($this->clustered) {
180             foreach ($this->setservers as $setserver) {
181                 // Since we will have a number of them with the same name, append server and port.
182                 $connection = new Memcache;
183                 $connection->addServer($setserver[0], $setserver[1]);
184                 $this->setconnections[] = $connection;
185             }
186         }
188         // Test the connection to the pool of servers.
189         $this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
190     }
192     /**
193      * Initialises the cache.
194      *
195      * Once this has been done the cache is all set to be used.
196      *
197      * @param cache_definition $definition
198      */
199     public function initialise(cache_definition $definition) {
200         if ($this->is_initialised()) {
201             throw new coding_exception('This memcache instance has already been initialised.');
202         }
203         $this->definition = $definition;
204         $this->isinitialised = true;
205     }
207     /**
208      * Returns true once this instance has been initialised.
209      *
210      * @return bool
211      */
212     public function is_initialised() {
213         return ($this->isinitialised);
214     }
216     /**
217      * Returns true if this store instance is ready to be used.
218      * @return bool
219      */
220     public function is_ready() {
221         return $this->isready;
222     }
224     /**
225      * Returns true if the store requirements are met.
226      *
227      * @return bool
228      */
229     public static function are_requirements_met() {
230         return class_exists('Memcache');
231     }
233     /**
234      * Returns true if the given mode is supported by this store.
235      *
236      * @param int $mode One of cache_store::MODE_*
237      * @return bool
238      */
239     public static function is_supported_mode($mode) {
240         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
241     }
243     /**
244      * Returns the supported features as a combined int.
245      *
246      * @param array $configuration
247      * @return int
248      */
249     public static function get_supported_features(array $configuration = array()) {
250         return self::SUPPORTS_NATIVE_TTL;
251     }
253     /**
254      * Returns false as this store does not support multiple identifiers.
255      * (This optional function is a performance optimisation; it must be
256      * consistent with the value from get_supported_features.)
257      *
258      * @return bool False
259      */
260     public function supports_multiple_identifiers() {
261         return false;
262     }
264     /**
265      * Returns the supported modes as a combined int.
266      *
267      * @param array $configuration
268      * @return int
269      */
270     public static function get_supported_modes(array $configuration = array()) {
271         return self::MODE_APPLICATION;
272     }
274     /**
275      * Parses the given key to make it work for this memcache backend.
276      *
277      * @param string $key The raw key.
278      * @return string The resulting key.
279      */
280     protected function parse_key($key) {
281         if (strlen($key) > 245) {
282             $key = '_sha1_'.sha1($key);
283         }
284         $key = $this->prefix . $key;
285         return $key;
286     }
288     /**
289      * Retrieves an item from the cache store given its key.
290      *
291      * @param string $key The key to retrieve
292      * @return mixed The data that was associated with the key, or false if the key did not exist.
293      */
294     public function get($key) {
295         return $this->connection->get($this->parse_key($key));
296     }
298     /**
299      * Retrieves several items from the cache store in a single transaction.
300      *
301      * 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.
302      *
303      * @param array $keys The array of keys to retrieve
304      * @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
305      *      be set to false.
306      */
307     public function get_many($keys) {
308         $mkeys = array();
309         foreach ($keys as $key) {
310             $mkeys[$key] = $this->parse_key($key);
311         }
312         $result = $this->connection->get($mkeys);
313         if (!is_array($result)) {
314             $result = array();
315         }
316         $return = array();
317         foreach ($mkeys as $key => $mkey) {
318             if (!array_key_exists($mkey, $result)) {
319                 $return[$key] = false;
320             } else {
321                 $return[$key] = $result[$mkey];
322             }
323         }
324         return $return;
325     }
327     /**
328      * Sets an item in the cache given its key and data value.
329      *
330      * @param string $key The key to use.
331      * @param mixed $data The data to set.
332      * @return bool True if the operation was a success false otherwise.
333      */
334     public function set($key, $data) {
335         if ($this->clustered) {
336             $status = true;
337             foreach ($this->setconnections as $connection) {
338                 $status = $connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl())
339                         && $status;
340             }
341             return $status;
342         }
344         return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
345     }
347     /**
348      * Sets many items in the cache in a single transaction.
349      *
350      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
351      *      keys, 'key' and 'value'.
352      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
353      *      sent ... if they care that is.
354      */
355     public function set_many(array $keyvaluearray) {
356         $count = 0;
357         foreach ($keyvaluearray as $pair) {
358             if ($this->set($pair['key'], $pair['value'])) {
359                 $count++;
360             }
361         }
362         return $count;
363     }
365     /**
366      * Deletes an item from the cache store.
367      *
368      * @param string $key The key to delete.
369      * @return bool Returns true if the operation was a success, false otherwise.
370      */
371     public function delete($key) {
372         if ($this->clustered) {
373             $status = true;
374             foreach ($this->setconnections as $connection) {
375                 $status = $connection->delete($this->parse_key($key)) && $status;
376             }
377             return $status;
378         }
380         return $this->connection->delete($this->parse_key($key));
381     }
383     /**
384      * Deletes several keys from the cache in a single action.
385      *
386      * @param array $keys The keys to delete
387      * @return int The number of items successfully deleted.
388      */
389     public function delete_many(array $keys) {
390         $count = 0;
391         foreach ($keys as $key) {
392             if ($this->delete($key)) {
393                 $count++;
394             }
395         }
396         return $count;
397     }
399     /**
400      * Purges the cache deleting all items within it.
401      *
402      * @return boolean True on success. False otherwise.
403      */
404     public function purge() {
405         if ($this->isready) {
406             if ($this->clustered) {
407                 foreach ($this->setconnections as $connection) {
408                     $connection->flush();
409                 }
410             } else {
411                 $this->connection->flush();
412             }
413         }
415         return true;
416     }
418     /**
419      * Given the data from the add instance form this function creates a configuration array.
420      *
421      * @param stdClass $data
422      * @return array
423      */
424     public static function config_get_configuration_array($data) {
425         $lines = explode("\n", $data->servers);
426         $servers = array();
427         foreach ($lines as $line) {
428             // Trim surrounding colons and default whitespace.
429             $line = trim(trim($line), ":");
430             // Skip blank lines.
431             if ($line === '') {
432                 continue;
433             }
434             $servers[] = explode(':', $line, 3);
435         }
437         $clustered = false;
438         if (isset($data->clustered)) {
439             $clustered = true;
440         }
442         $lines = explode("\n", $data->setservers);
443         $setservers = array();
444         foreach ($lines as $line) {
445             // Trim surrounding colons and default whitespace.
446             $line = trim(trim($line), ":");
447             if ($line === '') {
448                 continue;
449             }
450             $setserver = explode(':', $line, 3);
451             // We don't use weights, so display a debug message.
452             if (count($setserver) > 2) {
453                 debugging('Memcache Set Server '.$setserver[0].' has too many parameters.');
454             }
455             $setservers[] = $setserver;
456         }
458         return array(
459             'servers' => $servers,
460             'prefix' => $data->prefix,
461             'clustered' => $clustered,
462             'setservers' => $setservers
463         );
464     }
466     /**
467      * Allows the cache store to set its data against the edit form before it is shown to the user.
468      *
469      * @param moodleform $editform
470      * @param array $config
471      */
472     public static function config_set_edit_form_data(moodleform $editform, array $config) {
473         $data = array();
474         if (!empty($config['servers'])) {
475             $servers = array();
476             foreach ($config['servers'] as $server) {
477                 $servers[] = join(":", $server);
478             }
479             $data['servers'] = join("\n", $servers);
480         }
481         if (!empty($config['prefix'])) {
482             $data['prefix'] = $config['prefix'];
483         } else {
484             $data['prefix'] = self::DEFAULT_PREFIX;
485         }
486         if (isset($config['clustered'])) {
487             $data['clustered'] = (bool)$config['clustered'];
488         }
489         if (!empty($config['setservers'])) {
490             $servers = array();
491             foreach ($config['setservers'] as $server) {
492                 $servers[] = join(":", $server);
493             }
494             $data['setservers'] = join("\n", $servers);
495         }
497         $editform->set_data($data);
498     }
500     /**
501      * Performs any necessary clean up when the store instance is being deleted.
502      */
503     public function instance_deleted() {
504         if ($this->connection) {
505             $connection = $this->connection;
506         } else {
507             $connection = new Memcache;
508             foreach ($this->servers as $server) {
509                 $connection->addServer($server[0], $server[1], true, $server[2]);
510             }
511         }
512         @$connection->flush();
513         unset($connection);
514         unset($this->connection);
515     }
517     /**
518      * Generates an instance of the cache store that can be used for testing.
519      *
520      * @param cache_definition $definition
521      * @return cachestore_memcache|false
522      */
523     public static function initialise_test_instance(cache_definition $definition) {
524         if (!self::are_requirements_met()) {
525             return false;
526         }
528         $config = get_config('cachestore_memcache');
529         if (empty($config->testservers)) {
530             return false;
531         }
533         $configuration = array();
534         $configuration['servers'] = explode("\n", $config->testservers);
535         if (!empty($config->testclustered)) {
536             $configuration['clustered'] = $config->testclustered;
537         }
538         if (!empty($config->testsetservers)) {
539             $configuration['setservers'] = explode("\n", $config->testsetservers);
540         }
542         $store = new cachestore_memcache('Test memcache', $configuration);
543         $store->initialise($definition);
545         return $store;
546     }
548     /**
549      * Creates a test instance for unit tests if possible.
550      * @param cache_definition $definition
551      * @return bool|cachestore_memcache
552      */
553     public static function initialise_unit_test_instance(cache_definition $definition) {
554         if (!self::are_requirements_met()) {
555             return false;
556         }
557         if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
558             return false;
559         }
560         $configuration = array();
561         $configuration['servers'] = explode("\n", TEST_CACHESTORE_MEMCACHE_TESTSERVERS);
563         $store = new cachestore_memcache('Test memcache', $configuration);
564         $store->initialise($definition);
566         return $store;
567     }
569     /**
570      * Returns the name of this instance.
571      * @return string
572      */
573     public function my_name() {
574         return $this->name;
575     }
577     /**
578      * Used to notify of configuration conflicts.
579      *
580      * The warnings returned here will be displayed on the cache configuration screen.
581      *
582      * @return string[] Returns an array of warnings (strings)
583      */
584     public function get_warnings() {
585         global $CFG;
586         $warnings = array();
587         if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
588             $bits = explode(':', $CFG->session_memcached_save_path, 3);
589             $host = array_shift($bits);
590             $port = (count($bits)) ? array_shift($bits) : '11211';
591             foreach ($this->servers as $server) {
592                 if ($server[0] === $host && $server[1] === $port) {
593                     $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcache', $this->my_name());
594                     break;
595                 }
596             }
597         }
598         return $warnings;
599     }