MDL-63050 cachestore_redis: Update hExists to check empty
[moodle.git] / cache / stores / redis / lib.php
blob83a58892e5bfd3387160f856d0ab25732ae27ef4
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.
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
20 * @package cachestore_redis
21 * @copyright 2013 Adam Durana
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 /**
28 * Redis Cache Store
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.
36 * @copyright 2013 Adam Durana
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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.
44 * @var string
46 protected $name;
48 /**
49 * The definition hash, used for hash key
51 * @var string
53 protected $hash;
55 /**
56 * Flag for readiness!
58 * @var boolean
60 protected $isready = false;
62 /**
63 * Cache definition for this store.
65 * @var cache_definition
67 protected $definition = null;
69 /**
70 * Connection to Redis for this store.
72 * @var Redis
74 protected $redis;
76 /**
77 * Serializer for this store.
79 * @var int
81 protected $serializer = Redis::SERIALIZER_PHP;
83 /**
84 * Determines if the requirements for this type of store are met.
86 * @return bool
88 public static function are_requirements_met() {
89 return class_exists('Redis');
92 /**
93 * Determines if this type of store supports a given mode.
95 * @param int $mode
96 * @return bool
98 public static function is_supported_mode($mode) {
99 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
103 * Get the features of this type of cache store.
105 * @param array $configuration
106 * @return int
108 public static function get_supported_features(array $configuration = array()) {
109 return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE;
113 * Get the supported modes of this type of cache store.
115 * @param array $configuration
116 * @return int
118 public static function get_supported_modes(array $configuration = array()) {
119 return self::MODE_APPLICATION + self::MODE_SESSION;
123 * Constructs an instance of this type of store.
125 * @param string $name
126 * @param array $configuration
128 public function __construct($name, array $configuration = array()) {
129 $this->name = $name;
131 if (!array_key_exists('server', $configuration) || empty($configuration['server'])) {
132 return;
134 if (array_key_exists('serializer', $configuration)) {
135 $this->serializer = (int)$configuration['serializer'];
137 $password = !empty($configuration['password']) ? $configuration['password'] : '';
138 $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
139 $this->redis = $this->new_redis($configuration['server'], $prefix, $password);
143 * Create a new Redis instance and
144 * connect to the server.
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
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];
159 if ($redis->connect($server, $port)) {
160 if (!empty($password)) {
161 $redis->auth($password);
163 $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer);
164 if (!empty($prefix)) {
165 $redis->setOption(Redis::OPT_PREFIX, $prefix);
167 // Database setting option...
168 $this->isready = $this->ping($redis);
169 } else {
170 $this->isready = false;
172 return $redis;
176 * See if we can ping Redis server
178 * @param Redis $redis
179 * @return bool
181 protected function ping(Redis $redis) {
182 try {
183 if ($redis->ping() === false) {
184 return false;
186 } catch (Exception $e) {
187 return false;
189 return true;
193 * Get the name of the store.
195 * @return string
197 public function my_name() {
198 return $this->name;
202 * Initialize the store.
204 * @param cache_definition $definition
205 * @return bool
207 public function initialise(cache_definition $definition) {
208 $this->definition = $definition;
209 $this->hash = $definition->generate_definition_hash();
210 return true;
214 * Determine if the store is initialized.
216 * @return bool
218 public function is_initialised() {
219 return ($this->definition !== null);
223 * Determine if the store is ready for use.
225 * @return bool
227 public function is_ready() {
228 return $this->isready;
232 * Get the value associated with a given key.
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.
237 public function get($key) {
238 return $this->redis->hGet($this->hash, $key);
242 * Get the values associated with a list of keys.
244 * @param array $keys The keys to get the values of.
245 * @return array An array of the values of the given keys.
247 public function get_many($keys) {
248 return $this->redis->hMGet($this->hash, $keys);
252 * Set the value of a key.
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.
258 public function set($key, $value) {
259 return ($this->redis->hSet($this->hash, $key, $value) !== false);
263 * Set the values of many keys.
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.
269 public function set_many(array $keyvaluearray) {
270 $pairs = [];
271 foreach ($keyvaluearray as $pair) {
272 $pairs[$pair['key']] = $pair['value'];
274 if ($this->redis->hMSet($this->hash, $pairs)) {
275 return count($pairs);
277 return 0;
281 * Delete the given key.
283 * @param string $key The key to delete.
284 * @return bool True if the delete operation succeeds, false otherwise.
286 public function delete($key) {
287 return ($this->redis->hDel($this->hash, $key) > 0);
291 * Delete many keys.
293 * @param array $keys The keys to delete.
294 * @return int The number of keys successfully deleted.
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);
303 * Purges all keys from the store.
305 * @return bool
307 public function purge() {
308 return ($this->redis->del($this->hash) !== false);
312 * Cleans up after an instance of the store.
314 public function instance_deleted() {
315 $this->purge();
316 $this->redis->close();
317 unset($this->redis);
321 * Determines if the store has a given key.
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.
327 public function has($key) {
328 return !empty($this->redis->hExists($this->hash, $key));
332 * Determines if the store has any of the keys in a list.
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.
338 public function has_any(array $keys) {
339 foreach ($keys as $key) {
340 if ($this->has($key)) {
341 return true;
344 return false;
348 * Determines if the store has all of the keys in a list.
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.
354 public function has_all(array $keys) {
355 foreach ($keys as $key) {
356 if (!$this->has($key)) {
357 return false;
360 return true;
364 * Tries to acquire a lock with a given name.
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.
371 public function acquire_lock($key, $ownerid) {
372 return $this->redis->setnx($key, $ownerid);
376 * Checks a lock with a given name and owner information.
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.
384 public function check_lock_state($key, $ownerid) {
385 $result = $this->redis->get($key);
386 if ($result === $ownerid) {
387 return true;
389 if ($result === false) {
390 return null;
392 return false;
396 * Finds all of the keys being used by this cache store instance.
398 * @return array of all keys in the hash as a numbered array.
400 public function find_all() {
401 return $this->redis->hKeys($this->hash);
405 * Finds all of the keys whose keys start with the given prefix.
407 * @param string $prefix
409 * @return array List of keys that match this prefix.
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;
418 return $return;
422 * Releases a given lock if the owner information matches.
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.
429 public function release_lock($key, $ownerid) {
430 if ($this->check_lock_state($key, $ownerid)) {
431 return ($this->redis->del($key) !== false);
433 return false;
437 * Creates a configuration array from given 'add instance' form data.
439 * @see cache_is_configurable
440 * @param stdClass $data
441 * @return array
443 public static function config_get_configuration_array($data) {
444 return array(
445 'server' => $data->server,
446 'prefix' => $data->prefix,
447 'password' => $data->password,
448 'serializer' => $data->serializer
453 * Sets form data from a configuration array.
455 * @see cache_is_configurable
456 * @param moodleform $editform
457 * @param array $config
459 public static function config_set_edit_form_data(moodleform $editform, array $config) {
460 $data = array();
461 $data['server'] = $config['server'];
462 $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : '';
463 $data['password'] = !empty($config['password']) ? $config['password'] : '';
464 if (!empty($config['serializer'])) {
465 $data['serializer'] = $config['serializer'];
467 $editform->set_data($data);
472 * Creates an instance of the store for testing.
474 * @param cache_definition $definition
475 * @return mixed An instance of the store, or false if an instance cannot be created.
477 public static function initialise_test_instance(cache_definition $definition) {
478 if (!self::are_requirements_met()) {
479 return false;
481 $config = get_config('cachestore_redis');
482 if (empty($config->test_server)) {
483 return false;
485 $configuration = array('server' => $config->test_server);
486 if (!empty($config->test_serializer)) {
487 $configuration['serializer'] = $config->test_serializer;
489 if (!empty($config->test_password)) {
490 $configuration['password'] = $config->test_password;
492 $cache = new cachestore_redis('Redis test', $configuration);
493 $cache->initialise($definition);
495 return $cache;
499 * Return configuration to use when unit testing.
501 * @return array
503 public static function unit_test_configuration() {
504 global $DB;
506 if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) {
507 throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration');
510 return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
511 'prefix' => $DB->get_prefix(),
516 * Returns true if this cache store instance is both suitable for testing, and ready for testing.
518 * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing.
520 * @return bool
522 public static function ready_to_be_used_for_testing() {
523 return defined('TEST_CACHESTORE_REDIS_TESTSERVERS');
527 * Gets an array of options to use as the serialiser.
528 * @return array
530 public static function config_get_serializer_options() {
531 $options = array(
532 Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis')
535 if (defined('Redis::SERIALIZER_IGBINARY')) {
536 $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis');
538 return $options;