Merge branch 'MDL-58311_master' of https://github.com/sk-unikent/moodle
[moodle.git] / cache / stores / redis / 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  * Redis Cache Store - Main library
19  *
20  * @package   cachestore_redis
21  * @copyright 2013 Adam Durana
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * Redis Cache Store
29  *
30  * To allow separation of definitions in Moodle and faster purging, each cache
31  * is implemented as a Redis hash.  That is a trade-off between having functionality of TTL
32  * and being able to manage many caches in a single redis instance.  Given the recommendation
33  * not to use TTL if at all possible and the benefits of having many stores in Redis using the
34  * hash configuration, the hash implementation has been used.
35  *
36  * @copyright   2013 Adam Durana
37  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class cachestore_redis extends cache_store implements cache_is_key_aware, cache_is_lockable,
40         cache_is_configurable, cache_is_searchable {
41     /**
42      * Name of this store.
43      *
44      * @var string
45      */
46     protected $name;
48     /**
49      * The definition hash, used for hash key
50      *
51      * @var string
52      */
53     protected $hash;
55     /**
56      * Flag for readiness!
57      *
58      * @var boolean
59      */
60     protected $isready = false;
62     /**
63      * Cache definition for this store.
64      *
65      * @var cache_definition
66      */
67     protected $definition = null;
69     /**
70      * Connection to Redis for this store.
71      *
72      * @var Redis
73      */
74     protected $redis;
76     /**
77      * Serializer for this store.
78      *
79      * @var int
80      */
81     protected $serializer = Redis::SERIALIZER_PHP;
83     /**
84      * Determines if the requirements for this type of store are met.
85      *
86      * @return bool
87      */
88     public static function are_requirements_met() {
89         return class_exists('Redis');
90     }
92     /**
93      * Determines if this type of store supports a given mode.
94      *
95      * @param int $mode
96      * @return bool
97      */
98     public static function is_supported_mode($mode) {
99         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
100     }
102     /**
103      * Get the features of this type of cache store.
104      *
105      * @param array $configuration
106      * @return int
107      */
108     public static function get_supported_features(array $configuration = array()) {
109         return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE;
110     }
112     /**
113      * Get the supported modes of this type of cache store.
114      *
115      * @param array $configuration
116      * @return int
117      */
118     public static function get_supported_modes(array $configuration = array()) {
119         return self::MODE_APPLICATION + self::MODE_SESSION;
120     }
122     /**
123      * Constructs an instance of this type of store.
124      *
125      * @param string $name
126      * @param array $configuration
127      */
128     public function __construct($name, array $configuration = array()) {
129         $this->name = $name;
131         if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
132             return;
133         }
134         if (array_key_exists('serializer', $configuration)) {
135             $this->serializer = (int)$configuration['serializer'];
136         }
137         $password = !empty($configuration['password']) ? $configuration['password'] : '';
138         $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
139         $this->redis = $this->new_redis($configuration['server'], $prefix, $password);
140     }
142     /**
143      * Create a new Redis instance and
144      * connect to the server.
145      *
146      * @param string $server The server connection string
147      * @param string $prefix The key prefix
148      * @param string $password The server connection password
149      * @return Redis
150      */
151     protected function new_redis($server, $prefix = '', $password = '') {
152         $redis = new Redis();
153         $port = null;
154         if (strpos($server, ':')) {
155             $serverconf = explode(':', $server);
156             $server = $serverconf[0];
157             $port = $serverconf[1];
158         }
159         if ($redis->connect($server, $port)) {
160             if (!empty($password)) {
161                 $redis->auth($password);
162             }
163             $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
164             if (!empty($prefix)) {
165                 $redis->setOption(Redis::OPT_PREFIX, $prefix);
166             }
167             // Database setting option...
168             $this->isready = $this->ping($redis);
169         } else {
170             $this->isready = false;
171         }
172         return $redis;
173     }
175     /**
176      * See if we can ping Redis server
177      *
178      * @param Redis $redis
179      * @return bool
180      */
181     protected function ping(Redis $redis) {
182         try {
183             if ($redis->ping() === false) {
184                 return false;
185             }
186         } catch (Exception $e) {
187             return false;
188         }
189         return true;
190     }
192     /**
193      * Get the name of the store.
194      *
195      * @return string
196      */
197     public function my_name() {
198         return $this->name;
199     }
201     /**
202      * Initialize the store.
203      *
204      * @param cache_definition $definition
205      * @return bool
206      */
207     public function initialise(cache_definition $definition) {
208         $this->definition = $definition;
209         $this->hash       = $definition->generate_definition_hash();
210         return true;
211     }
213     /**
214      * Determine if the store is initialized.
215      *
216      * @return bool
217      */
218     public function is_initialised() {
219         return ($this->definition !== null);
220     }
222     /**
223      * Determine if the store is ready for use.
224      *
225      * @return bool
226      */
227     public function is_ready() {
228         return $this->isready;
229     }
231     /**
232      * Get the value associated with a given key.
233      *
234      * @param string $key The key to get the value of.
235      * @return mixed The value of the key, or false if there is no value associated with the key.
236      */
237     public function get($key) {
238         return $this->redis->hGet($this->hash, $key);
239     }
241     /**
242      * Get the values associated with a list of keys.
243      *
244      * @param array $keys The keys to get the values of.
245      * @return array An array of the values of the given keys.
246      */
247     public function get_many($keys) {
248         return $this->redis->hMGet($this->hash, $keys);
249     }
251     /**
252      * Set the value of a key.
253      *
254      * @param string $key The key to set the value of.
255      * @param mixed $value The value.
256      * @return bool True if the operation succeeded, false otherwise.
257      */
258     public function set($key, $value) {
259         return ($this->redis->hSet($this->hash, $key, $value) !== false);
260     }
262     /**
263      * Set the values of many keys.
264      *
265      * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array
266      *      with two keys, 'key' and 'value'.
267      * @return int The number of key/value pairs successfuly set.
268      */
269     public function set_many(array $keyvaluearray) {
270         $pairs = [];
271         foreach ($keyvaluearray as $pair) {
272             $pairs[$pair['key']] = $pair['value'];
273         }
274         if ($this->redis->hMSet($this->hash, $pairs)) {
275             return count($pairs);
276         }
277         return 0;
278     }
280     /**
281      * Delete the given key.
282      *
283      * @param string $key The key to delete.
284      * @return bool True if the delete operation succeeds, false otherwise.
285      */
286     public function delete($key) {
287         return ($this->redis->hDel($this->hash, $key) > 0);
288     }
290     /**
291      * Delete many keys.
292      *
293      * @param array $keys The keys to delete.
294      * @return int The number of keys successfully deleted.
295      */
296     public function delete_many(array $keys) {
297         // Redis needs the hash as the first argument, so we have to put it at the start of the array.
298         array_unshift($keys, $this->hash);
299         return call_user_func_array(array($this->redis, 'hDel'), $keys);
300     }
302     /**
303      * Purges all keys from the store.
304      *
305      * @return bool
306      */
307     public function purge() {
308         return ($this->redis->del($this->hash) !== false);
309     }
311     /**
312      * Cleans up after an instance of the store.
313      */
314     public function instance_deleted() {
315         $this->purge();
316         $this->redis->close();
317         unset($this->redis);
318     }
320     /**
321      * Determines if the store has a given key.
322      *
323      * @see cache_is_key_aware
324      * @param string $key The key to check for.
325      * @return bool True if the key exists, false if it does not.
326      */
327     public function has($key) {
328         return $this->redis->hExists($this->hash, $key);
329     }
331     /**
332      * Determines if the store has any of the keys in a list.
333      *
334      * @see cache_is_key_aware
335      * @param array $keys The keys to check for.
336      * @return bool True if any of the keys are found, false none of the keys are found.
337      */
338     public function has_any(array $keys) {
339         foreach ($keys as $key) {
340             if ($this->has($key)) {
341                 return true;
342             }
343         }
344         return false;
345     }
347     /**
348      * Determines if the store has all of the keys in a list.
349      *
350      * @see cache_is_key_aware
351      * @param array $keys The keys to check for.
352      * @return bool True if all of the keys are found, false otherwise.
353      */
354     public function has_all(array $keys) {
355         foreach ($keys as $key) {
356             if (!$this->has($key)) {
357                 return false;
358             }
359         }
360         return true;
361     }
363     /**
364      * Tries to acquire a lock with a given name.
365      *
366      * @see cache_is_lockable
367      * @param string $key Name of the lock to acquire.
368      * @param string $ownerid Information to identify owner of lock if acquired.
369      * @return bool True if the lock was acquired, false if it was not.
370      */
371     public function acquire_lock($key, $ownerid) {
372         return $this->redis->setnx($key, $ownerid);
373     }
375     /**
376      * Checks a lock with a given name and owner information.
377      *
378      * @see cache_is_lockable
379      * @param string $key Name of the lock to check.
380      * @param string $ownerid Owner information to check existing lock against.
381      * @return mixed True if the lock exists and the owner information matches, null if the lock does not
382      *      exist, and false otherwise.
383      */
384     public function check_lock_state($key, $ownerid) {
385         $result = $this->redis->get($key);
386         if ($result === $ownerid) {
387             return true;
388         }
389         if ($result === false) {
390             return null;
391         }
392         return false;
393     }
395     /**
396      * Finds all of the keys being used by this cache store instance.
397      *
398      * @return array of all keys in the hash as a numbered array.
399      */
400     public function find_all() {
401         return $this->redis->hKeys($this->hash);
402     }
404     /**
405      * Finds all of the keys whose keys start with the given prefix.
406      *
407      * @param string $prefix
408      *
409      * @return array List of keys that match this prefix.
410      */
411     public function find_by_prefix($prefix) {
412         $return = [];
413         foreach ($this->find_all() as $key) {
414             if (strpos($key, $prefix) === 0) {
415                 $return[] = $key;
416             }
417         }
418         return $return;
419     }
421     /**
422      * Releases a given lock if the owner information matches.
423      *
424      * @see cache_is_lockable
425      * @param string $key Name of the lock to release.
426      * @param string $ownerid Owner information to use.
427      * @return bool True if the lock is released, false if it is not.
428      */
429     public function release_lock($key, $ownerid) {
430         if ($this->check_lock_state($key, $ownerid)) {
431             return ($this->redis->del($key) !== false);
432         }
433         return false;
434     }
436     /**
437      * Creates a configuration array from given 'add instance' form data.
438      *
439      * @see cache_is_configurable
440      * @param stdClass $data
441      * @return array
442      */
443     public static function config_get_configuration_array($data) {
444         return array(
445             'server' => $data->server,
446             'prefix' => $data->prefix,
447             'serializer' => $data->serializer
448         );
449     }
451     /**
452      * Sets form data from a configuration array.
453      *
454      * @see cache_is_configurable
455      * @param moodleform $editform
456      * @param array $config
457      */
458     public static function config_set_edit_form_data(moodleform $editform, array $config) {
459         $data = array();
460         $data['server'] = $config['server'];
461         $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
462         if (!empty($config['serializer'])) {
463             $data['serializer'] = $config['serializer'];
464         }
465         $editform->set_data($data);
466     }
469     /**
470      * Creates an instance of the store for testing.
471      *
472      * @param cache_definition $definition
473      * @return mixed An instance of the store, or false if an instance cannot be created.
474      */
475     public static function initialise_test_instance(cache_definition $definition) {
476         if (!self::are_requirements_met()) {
477             return false;
478         }
479         $config = get_config('cachestore_redis');
480         if (empty($config->test_server)) {
481             return false;
482         }
483         $configuration = array('server' => $config->test_server);
484         if (!empty($config->test_serializer)) {
485             $configuration['serializer'] = $config->test_serializer;
486         }
487         $cache = new cachestore_redis('Redis test', $configuration);
488         $cache->initialise($definition);
490         return $cache;
491     }
493     /**
494      * Return configuration to use when unit testing.
495      *
496      * @return array
497      */
498     public static function unit_test_configuration() {
499         global $DB;
501         if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
502             throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
503         }
505         return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
506                 'prefix' => $DB->get_prefix(),
507         ];
508     }
510     /**
511      * Returns true if this cache store instance is both suitable for testing, and ready for testing.
512      *
513      * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
514      *
515      * @return bool
516      */
517     public static function ready_to_be_used_for_testing() {
518         return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
519     }
521     /**
522      * Gets an array of options to use as the serialiser.
523      * @return array
524      */
525     public static function config_get_serializer_options() {
526         $options = array(
527             Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis')
528         );
530         if (defined('Redis::SERIALIZER_IGBINARY')) {
531             $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis');
532         }
533         return $options;
534     }