Merge branch 'wip-mdl-58068' of https://github.com/rajeshtaneja/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         $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
138         $this->redis = $this->new_redis($configuration['server'], $prefix);
139     }
141     /**
142      * Create a new Redis instance and
143      * connect to the server.
144      *
145      * @param string $server The server connection string
146      * @param string $prefix The key prefix
147      * @return Redis
148      */
149     protected function new_redis($server, $prefix = '') {
150         $redis = new Redis();
151         $port = null;
152         if (strpos($server, ':')) {
153             $serverconf = explode(':', $server);
154             $server = $serverconf[0];
155             $port = $serverconf[1];
156         }
157         if ($redis->connect($server, $port)) {
158             $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
159             if (!empty($prefix)) {
160                 $redis->setOption(Redis::OPT_PREFIX, $prefix);
161             }
162             // Database setting option...
163             $this->isready = $this->ping($redis);
164         } else {
165             $this->isready = false;
166         }
167         return $redis;
168     }
170     /**
171      * See if we can ping Redis server
172      *
173      * @param Redis $redis
174      * @return bool
175      */
176     protected function ping(Redis $redis) {
177         try {
178             if ($redis->ping() === false) {
179                 return false;
180             }
181         } catch (Exception $e) {
182             return false;
183         }
184         return true;
185     }
187     /**
188      * Get the name of the store.
189      *
190      * @return string
191      */
192     public function my_name() {
193         return $this->name;
194     }
196     /**
197      * Initialize the store.
198      *
199      * @param cache_definition $definition
200      * @return bool
201      */
202     public function initialise(cache_definition $definition) {
203         $this->definition = $definition;
204         $this->hash       = $definition->generate_definition_hash();
205         return true;
206     }
208     /**
209      * Determine if the store is initialized.
210      *
211      * @return bool
212      */
213     public function is_initialised() {
214         return ($this->definition !== null);
215     }
217     /**
218      * Determine if the store is ready for use.
219      *
220      * @return bool
221      */
222     public function is_ready() {
223         return $this->isready;
224     }
226     /**
227      * Get the value associated with a given key.
228      *
229      * @param string $key The key to get the value of.
230      * @return mixed The value of the key, or false if there is no value associated with the key.
231      */
232     public function get($key) {
233         return $this->redis->hGet($this->hash, $key);
234     }
236     /**
237      * Get the values associated with a list of keys.
238      *
239      * @param array $keys The keys to get the values of.
240      * @return array An array of the values of the given keys.
241      */
242     public function get_many($keys) {
243         return $this->redis->hMGet($this->hash, $keys);
244     }
246     /**
247      * Set the value of a key.
248      *
249      * @param string $key The key to set the value of.
250      * @param mixed $value The value.
251      * @return bool True if the operation succeeded, false otherwise.
252      */
253     public function set($key, $value) {
254         return ($this->redis->hSet($this->hash, $key, $value) !== false);
255     }
257     /**
258      * Set the values of many keys.
259      *
260      * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array
261      *      with two keys, 'key' and 'value'.
262      * @return int The number of key/value pairs successfuly set.
263      */
264     public function set_many(array $keyvaluearray) {
265         $pairs = [];
266         foreach ($keyvaluearray as $pair) {
267             $pairs[$pair['key']] = $pair['value'];
268         }
269         if ($this->redis->hMSet($this->hash, $pairs)) {
270             return count($pairs);
271         }
272         return 0;
273     }
275     /**
276      * Delete the given key.
277      *
278      * @param string $key The key to delete.
279      * @return bool True if the delete operation succeeds, false otherwise.
280      */
281     public function delete($key) {
282         return ($this->redis->hDel($this->hash, $key) > 0);
283     }
285     /**
286      * Delete many keys.
287      *
288      * @param array $keys The keys to delete.
289      * @return int The number of keys successfully deleted.
290      */
291     public function delete_many(array $keys) {
292         // Redis needs the hash as the first argument, so we have to put it at the start of the array.
293         array_unshift($keys, $this->hash);
294         return call_user_func_array(array($this->redis, 'hDel'), $keys);
295     }
297     /**
298      * Purges all keys from the store.
299      *
300      * @return bool
301      */
302     public function purge() {
303         return ($this->redis->del($this->hash) !== false);
304     }
306     /**
307      * Cleans up after an instance of the store.
308      */
309     public function instance_deleted() {
310         $this->purge();
311         $this->redis->close();
312         unset($this->redis);
313     }
315     /**
316      * Determines if the store has a given key.
317      *
318      * @see cache_is_key_aware
319      * @param string $key The key to check for.
320      * @return bool True if the key exists, false if it does not.
321      */
322     public function has($key) {
323         return $this->redis->hExists($this->hash, $key);
324     }
326     /**
327      * Determines if the store has any of the keys in a list.
328      *
329      * @see cache_is_key_aware
330      * @param array $keys The keys to check for.
331      * @return bool True if any of the keys are found, false none of the keys are found.
332      */
333     public function has_any(array $keys) {
334         foreach ($keys as $key) {
335             if ($this->has($key)) {
336                 return true;
337             }
338         }
339         return false;
340     }
342     /**
343      * Determines if the store has all of the keys in a list.
344      *
345      * @see cache_is_key_aware
346      * @param array $keys The keys to check for.
347      * @return bool True if all of the keys are found, false otherwise.
348      */
349     public function has_all(array $keys) {
350         foreach ($keys as $key) {
351             if (!$this->has($key)) {
352                 return false;
353             }
354         }
355         return true;
356     }
358     /**
359      * Tries to acquire a lock with a given name.
360      *
361      * @see cache_is_lockable
362      * @param string $key Name of the lock to acquire.
363      * @param string $ownerid Information to identify owner of lock if acquired.
364      * @return bool True if the lock was acquired, false if it was not.
365      */
366     public function acquire_lock($key, $ownerid) {
367         return $this->redis->setnx($key, $ownerid);
368     }
370     /**
371      * Checks a lock with a given name and owner information.
372      *
373      * @see cache_is_lockable
374      * @param string $key Name of the lock to check.
375      * @param string $ownerid Owner information to check existing lock against.
376      * @return mixed True if the lock exists and the owner information matches, null if the lock does not
377      *      exist, and false otherwise.
378      */
379     public function check_lock_state($key, $ownerid) {
380         $result = $this->redis->get($key);
381         if ($result === $ownerid) {
382             return true;
383         }
384         if ($result === false) {
385             return null;
386         }
387         return false;
388     }
390     /**
391      * Finds all of the keys being used by this cache store instance.
392      *
393      * @return array of all keys in the hash as a numbered array.
394      */
395     public function find_all() {
396         return $this->redis->hKeys($this->hash);
397     }
399     /**
400      * Finds all of the keys whose keys start with the given prefix.
401      *
402      * @param string $prefix
403      *
404      * @return array List of keys that match this prefix.
405      */
406     public function find_by_prefix($prefix) {
407         $return = [];
408         foreach ($this->find_all() as $key) {
409             if (strpos($key, $prefix) === 0) {
410                 $return[] = $key;
411             }
412         }
413         return $return;
414     }
416     /**
417      * Releases a given lock if the owner information matches.
418      *
419      * @see cache_is_lockable
420      * @param string $key Name of the lock to release.
421      * @param string $ownerid Owner information to use.
422      * @return bool True if the lock is released, false if it is not.
423      */
424     public function release_lock($key, $ownerid) {
425         if ($this->check_lock_state($key, $ownerid)) {
426             return ($this->redis->del($key) !== false);
427         }
428         return false;
429     }
431     /**
432      * Creates a configuration array from given 'add instance' form data.
433      *
434      * @see cache_is_configurable
435      * @param stdClass $data
436      * @return array
437      */
438     public static function config_get_configuration_array($data) {
439         return array(
440             'server' => $data->server,
441             'prefix' => $data->prefix,
442             'serializer' => $data->serializer
443         );
444     }
446     /**
447      * Sets form data from a configuration array.
448      *
449      * @see cache_is_configurable
450      * @param moodleform $editform
451      * @param array $config
452      */
453     public static function config_set_edit_form_data(moodleform $editform, array $config) {
454         $data = array();
455         $data['server'] = $config['server'];
456         $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
457         if (!empty($config['serializer'])) {
458             $data['serializer'] = $config['serializer'];
459         }
460         $editform->set_data($data);
461     }
464     /**
465      * Creates an instance of the store for testing.
466      *
467      * @param cache_definition $definition
468      * @return mixed An instance of the store, or false if an instance cannot be created.
469      */
470     public static function initialise_test_instance(cache_definition $definition) {
471         if (!self::are_requirements_met()) {
472             return false;
473         }
474         $config = get_config('cachestore_redis');
475         if (empty($config->test_server)) {
476             return false;
477         }
478         $configuration = array('server' => $config->test_server);
479         if (!empty($config->test_serializer)) {
480             $configuration['serializer'] = $config->test_serializer;
481         }
482         $cache = new cachestore_redis('Redis test', $configuration);
483         $cache->initialise($definition);
485         return $cache;
486     }
488     /**
489      * Return configuration to use when unit testing.
490      *
491      * @return array
492      */
493     public static function unit_test_configuration() {
494         global $DB;
496         if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
497             throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
498         }
500         return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
501                 'prefix' => $DB->get_prefix(),
502         ];
503     }
505     /**
506      * Returns true if this cache store instance is both suitable for testing, and ready for testing.
507      *
508      * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
509      *
510      * @return bool
511      */
512     public static function ready_to_be_used_for_testing() {
513         return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
514     }
516     /**
517      * Gets an array of options to use as the serialiser.
518      * @return array
519      */
520     public static function config_get_serializer_options() {
521         $options = array(
522             Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis')
523         );
525         if (defined('Redis::SERIALIZER_IGBINARY')) {
526             $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis');
527         }
528         return $options;
529     }