Differences Between: [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
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/>. 16 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 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 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 * Compressor: none. 43 */ 44 const COMPRESSOR_NONE = 0; 45 46 /** 47 * Compressor: PHP GZip. 48 */ 49 const COMPRESSOR_PHP_GZIP = 1; 50 51 /** 52 * Compressor: PHP Zstandard. 53 */ 54 const COMPRESSOR_PHP_ZSTD = 2; 55 56 /** 57 * Name of this store. 58 * 59 * @var string 60 */ 61 protected $name; 62 63 /** 64 * The definition hash, used for hash key 65 * 66 * @var string 67 */ 68 protected $hash; 69 70 /** 71 * Flag for readiness! 72 * 73 * @var boolean 74 */ 75 protected $isready = false; 76 77 /** 78 * Cache definition for this store. 79 * 80 * @var cache_definition 81 */ 82 protected $definition = null; 83 84 /** 85 * Connection to Redis for this store. 86 * 87 * @var Redis 88 */ 89 protected $redis; 90 91 /** 92 * Serializer for this store. 93 * 94 * @var int 95 */ 96 protected $serializer = Redis::SERIALIZER_PHP; 97 98 /** 99 * Compressor for this store. 100 * 101 * @var int 102 */ 103 protected $compressor = self::COMPRESSOR_NONE; 104 105 /** 106 * Determines if the requirements for this type of store are met. 107 * 108 * @return bool 109 */ 110 public static function are_requirements_met() { 111 return class_exists('Redis'); 112 } 113 114 /** 115 * Determines if this type of store supports a given mode. 116 * 117 * @param int $mode 118 * @return bool 119 */ 120 public static function is_supported_mode($mode) { 121 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); 122 } 123 124 /** 125 * Get the features of this type of cache store. 126 * 127 * @param array $configuration 128 * @return int 129 */ 130 public static function get_supported_features(array $configuration = array()) { 131 return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE; 132 } 133 134 /** 135 * Get the supported modes of this type of cache store. 136 * 137 * @param array $configuration 138 * @return int 139 */ 140 public static function get_supported_modes(array $configuration = array()) { 141 return self::MODE_APPLICATION + self::MODE_SESSION; 142 } 143 144 /** 145 * Constructs an instance of this type of store. 146 * 147 * @param string $name 148 * @param array $configuration 149 */ 150 public function __construct($name, array $configuration = array()) { 151 $this->name = $name; 152 153 if (!array_key_exists('server', $configuration) || empty($configuration['server'])) { 154 return; 155 } 156 if (array_key_exists('serializer', $configuration)) { 157 $this->serializer = (int)$configuration['serializer']; 158 } 159 if (array_key_exists('compressor', $configuration)) { 160 $this->compressor = (int)$configuration['compressor']; 161 } 162 $password = !empty($configuration['password']) ? $configuration['password'] : ''; 163 $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : ''; 164 $this->redis = $this->new_redis($configuration['server'], $prefix, $password); 165 } 166 167 /** 168 * Create a new Redis instance and 169 * connect to the server. 170 * 171 * @param string $server The server connection string 172 * @param string $prefix The key prefix 173 * @param string $password The server connection password 174 * @return Redis 175 */ 176 protected function new_redis($server, $prefix = '', $password = '') { 177 $redis = new Redis(); 178 // Check if it isn't a Unix socket to set default port. 179 $port = ($server[0] === '/') ? null : 6379; 180 if (strpos($server, ':')) { 181 $serverconf = explode(':', $server); 182 $server = $serverconf[0]; 183 $port = $serverconf[1]; 184 } 185 186 try { 187 if ($redis->connect($server, $port)) { 188 if (!empty($password)) { 189 $redis->auth($password); 190 } 191 // If using compressor, serialisation will be done at cachestore level, not php-redis. 192 if ($this->compressor == self::COMPRESSOR_NONE) { 193 $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer); 194 } 195 if (!empty($prefix)) { 196 $redis->setOption(Redis::OPT_PREFIX, $prefix); 197 } 198 // Database setting option... 199 $this->isready = $this->ping($redis); 200 } else { 201 $this->isready = false; 202 } 203 } catch (\RedisException $e) { 204 $this->isready = false; 205 } 206 207 return $redis; 208 } 209 210 /** 211 * See if we can ping Redis server 212 * 213 * @param Redis $redis 214 * @return bool 215 */ 216 protected function ping(Redis $redis) { 217 try { 218 if ($redis->ping() === false) { 219 return false; 220 } 221 } catch (Exception $e) { 222 return false; 223 } 224 return true; 225 } 226 227 /** 228 * Get the name of the store. 229 * 230 * @return string 231 */ 232 public function my_name() { 233 return $this->name; 234 } 235 236 /** 237 * Initialize the store. 238 * 239 * @param cache_definition $definition 240 * @return bool 241 */ 242 public function initialise(cache_definition $definition) { 243 $this->definition = $definition; 244 $this->hash = $definition->generate_definition_hash(); 245 return true; 246 } 247 248 /** 249 * Determine if the store is initialized. 250 * 251 * @return bool 252 */ 253 public function is_initialised() { 254 return ($this->definition !== null); 255 } 256 257 /** 258 * Determine if the store is ready for use. 259 * 260 * @return bool 261 */ 262 public function is_ready() { 263 return $this->isready; 264 } 265 266 /** 267 * Get the value associated with a given key. 268 * 269 * @param string $key The key to get the value of. 270 * @return mixed The value of the key, or false if there is no value associated with the key. 271 */ 272 public function get($key) { 273 $value = $this->redis->hGet($this->hash, $key); 274 275 if ($this->compressor == self::COMPRESSOR_NONE) { 276 return $value; 277 } 278 279 return $this->uncompress($value); 280 } 281 282 /** 283 * Get the values associated with a list of keys. 284 * 285 * @param array $keys The keys to get the values of. 286 * @return array An array of the values of the given keys. 287 */ 288 public function get_many($keys) { 289 $values = $this->redis->hMGet($this->hash, $keys); 290 291 if ($this->compressor == self::COMPRESSOR_NONE) { 292 return $values; 293 } 294 295 foreach ($values as &$value) { 296 $value = $this->uncompress($value); 297 } 298 299 return $values; 300 } 301 302 /** 303 * Set the value of a key. 304 * 305 * @param string $key The key to set the value of. 306 * @param mixed $value The value. 307 * @return bool True if the operation succeeded, false otherwise. 308 */ 309 public function set($key, $value) { 310 if ($this->compressor != self::COMPRESSOR_NONE) { 311 $value = $this->compress($value); 312 } 313 314 return ($this->redis->hSet($this->hash, $key, $value) !== false); 315 } 316 317 /** 318 * Set the values of many keys. 319 * 320 * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array 321 * with two keys, 'key' and 'value'. 322 * @return int The number of key/value pairs successfuly set. 323 */ 324 public function set_many(array $keyvaluearray) { 325 $pairs = []; 326 foreach ($keyvaluearray as $pair) { 327 $key = $pair['key']; 328 if ($this->compressor != self::COMPRESSOR_NONE) { 329 $pairs[$key] = $this->compress($pair['value']); 330 } else { 331 $pairs[$key] = $pair['value']; 332 } 333 } 334 if ($this->redis->hMSet($this->hash, $pairs)) { 335 return count($pairs); 336 } 337 return 0; 338 } 339 340 /** 341 * Delete the given key. 342 * 343 * @param string $key The key to delete. 344 * @return bool True if the delete operation succeeds, false otherwise. 345 */ 346 public function delete($key) { 347 return ($this->redis->hDel($this->hash, $key) > 0); 348 } 349 350 /** 351 * Delete many keys. 352 * 353 * @param array $keys The keys to delete. 354 * @return int The number of keys successfully deleted. 355 */ 356 public function delete_many(array $keys) { 357 // Redis needs the hash as the first argument, so we have to put it at the start of the array. 358 array_unshift($keys, $this->hash); 359 return call_user_func_array(array($this->redis, 'hDel'), $keys); 360 } 361 362 /** 363 * Purges all keys from the store. 364 * 365 * @return bool 366 */ 367 public function purge() { 368 return ($this->redis->del($this->hash) !== false); 369 } 370 371 /** 372 * Cleans up after an instance of the store. 373 */ 374 public function instance_deleted() { 375 $this->purge(); 376 $this->redis->close(); 377 unset($this->redis); 378 } 379 380 /** 381 * Determines if the store has a given key. 382 * 383 * @see cache_is_key_aware 384 * @param string $key The key to check for. 385 * @return bool True if the key exists, false if it does not. 386 */ 387 public function has($key) { 388 return !empty($this->redis->hExists($this->hash, $key)); 389 } 390 391 /** 392 * Determines if the store has any of the keys in a list. 393 * 394 * @see cache_is_key_aware 395 * @param array $keys The keys to check for. 396 * @return bool True if any of the keys are found, false none of the keys are found. 397 */ 398 public function has_any(array $keys) { 399 foreach ($keys as $key) { 400 if ($this->has($key)) { 401 return true; 402 } 403 } 404 return false; 405 } 406 407 /** 408 * Determines if the store has all of the keys in a list. 409 * 410 * @see cache_is_key_aware 411 * @param array $keys The keys to check for. 412 * @return bool True if all of the keys are found, false otherwise. 413 */ 414 public function has_all(array $keys) { 415 foreach ($keys as $key) { 416 if (!$this->has($key)) { 417 return false; 418 } 419 } 420 return true; 421 } 422 423 /** 424 * Tries to acquire a lock with a given name. 425 * 426 * @see cache_is_lockable 427 * @param string $key Name of the lock to acquire. 428 * @param string $ownerid Information to identify owner of lock if acquired. 429 * @return bool True if the lock was acquired, false if it was not. 430 */ 431 public function acquire_lock($key, $ownerid) { 432 return $this->redis->setnx($key, $ownerid); 433 } 434 435 /** 436 * Checks a lock with a given name and owner information. 437 * 438 * @see cache_is_lockable 439 * @param string $key Name of the lock to check. 440 * @param string $ownerid Owner information to check existing lock against. 441 * @return mixed True if the lock exists and the owner information matches, null if the lock does not 442 * exist, and false otherwise. 443 */ 444 public function check_lock_state($key, $ownerid) { 445 $result = $this->redis->get($key); 446 if ($result === $ownerid) { 447 return true; 448 } 449 if ($result === false) { 450 return null; 451 } 452 return false; 453 } 454 455 /** 456 * Finds all of the keys being used by this cache store instance. 457 * 458 * @return array of all keys in the hash as a numbered array. 459 */ 460 public function find_all() { 461 return $this->redis->hKeys($this->hash); 462 } 463 464 /** 465 * Finds all of the keys whose keys start with the given prefix. 466 * 467 * @param string $prefix 468 * 469 * @return array List of keys that match this prefix. 470 */ 471 public function find_by_prefix($prefix) { 472 $return = []; 473 foreach ($this->find_all() as $key) { 474 if (strpos($key, $prefix) === 0) { 475 $return[] = $key; 476 } 477 } 478 return $return; 479 } 480 481 /** 482 * Releases a given lock if the owner information matches. 483 * 484 * @see cache_is_lockable 485 * @param string $key Name of the lock to release. 486 * @param string $ownerid Owner information to use. 487 * @return bool True if the lock is released, false if it is not. 488 */ 489 public function release_lock($key, $ownerid) { 490 if ($this->check_lock_state($key, $ownerid)) { 491 return ($this->redis->del($key) !== false); 492 } 493 return false; 494 } 495 496 /** 497 * Creates a configuration array from given 'add instance' form data. 498 * 499 * @see cache_is_configurable 500 * @param stdClass $data 501 * @return array 502 */ 503 public static function config_get_configuration_array($data) { 504 return array( 505 'server' => $data->server, 506 'prefix' => $data->prefix, 507 'password' => $data->password, 508 'serializer' => $data->serializer, 509 'compressor' => $data->compressor, 510 ); 511 } 512 513 /** 514 * Sets form data from a configuration array. 515 * 516 * @see cache_is_configurable 517 * @param moodleform $editform 518 * @param array $config 519 */ 520 public static function config_set_edit_form_data(moodleform $editform, array $config) { 521 $data = array(); 522 $data['server'] = $config['server']; 523 $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : ''; 524 $data['password'] = !empty($config['password']) ? $config['password'] : ''; 525 if (!empty($config['serializer'])) { 526 $data['serializer'] = $config['serializer']; 527 } 528 if (!empty($config['compressor'])) { 529 $data['compressor'] = $config['compressor']; 530 } 531 $editform->set_data($data); 532 } 533 534 535 /** 536 * Creates an instance of the store for testing. 537 * 538 * @param cache_definition $definition 539 * @return mixed An instance of the store, or false if an instance cannot be created. 540 */ 541 public static function initialise_test_instance(cache_definition $definition) { 542 if (!self::are_requirements_met()) { 543 return false; 544 } 545 $config = get_config('cachestore_redis'); 546 if (empty($config->test_server)) { 547 return false; 548 } 549 $configuration = array('server' => $config->test_server); 550 if (!empty($config->test_serializer)) { 551 $configuration['serializer'] = $config->test_serializer; 552 } 553 if (!empty($config->test_password)) { 554 $configuration['password'] = $config->test_password; 555 } 556 $cache = new cachestore_redis('Redis test', $configuration); 557 $cache->initialise($definition); 558 559 return $cache; 560 } 561 562 /** 563 * Return configuration to use when unit testing. 564 * 565 * @return array 566 */ 567 public static function unit_test_configuration() { 568 global $DB; 569 570 if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) { 571 throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration'); 572 } 573 574 return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS, 575 'prefix' => $DB->get_prefix(), 576 ]; 577 } 578 579 /** 580 * Returns true if this cache store instance is both suitable for testing, and ready for testing. 581 * 582 * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing. 583 * 584 * @return bool 585 */ 586 public static function ready_to_be_used_for_testing() { 587 return defined('TEST_CACHESTORE_REDIS_TESTSERVERS'); 588 } 589 590 /** 591 * Gets an array of options to use as the serialiser. 592 * @return array 593 */ 594 public static function config_get_serializer_options() { 595 $options = array( 596 Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis') 597 ); 598 599 if (defined('Redis::SERIALIZER_IGBINARY')) { 600 $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis'); 601 } 602 return $options; 603 } 604 605 /** 606 * Gets an array of options to use as the compressor. 607 * 608 * @return array 609 */ 610 public static function config_get_compressor_options() { 611 $arr = [ 612 self::COMPRESSOR_NONE => get_string('compressor_none', 'cachestore_redis'), 613 self::COMPRESSOR_PHP_GZIP => get_string('compressor_php_gzip', 'cachestore_redis'), 614 ]; 615 616 // Check if the Zstandard PHP extension is installed. 617 if (extension_loaded('zstd')) { 618 $arr[self::COMPRESSOR_PHP_ZSTD] = get_string('compressor_php_zstd', 'cachestore_redis'); 619 } 620 621 return $arr; 622 } 623 624 /** 625 * Compress the given value, serializing it first. 626 * 627 * @param mixed $value 628 * @return string 629 */ 630 private function compress($value) { 631 $value = $this->serialize($value); 632 633 switch ($this->compressor) { 634 case self::COMPRESSOR_NONE: 635 return $value; 636 637 case self::COMPRESSOR_PHP_GZIP: 638 return gzencode($value); 639 640 case self::COMPRESSOR_PHP_ZSTD: 641 return zstd_compress($value); 642 643 default: 644 debugging("Invalid compressor: {$this->compressor}"); 645 return $value; 646 } 647 } 648 649 /** 650 * Uncompresses (deflates) the data, unserialising it afterwards. 651 * 652 * @param string $value 653 * @return mixed 654 */ 655 private function uncompress($value) { 656 if ($value === false) { 657 return false; 658 } 659 660 switch ($this->compressor) { 661 case self::COMPRESSOR_NONE: 662 break; 663 case self::COMPRESSOR_PHP_GZIP: 664 $value = gzdecode($value); 665 break; 666 case self::COMPRESSOR_PHP_ZSTD: 667 $value = zstd_uncompress($value); 668 break; 669 default: 670 debugging("Invalid compressor: {$this->compressor}"); 671 } 672 673 return $this->unserialize($value); 674 } 675 676 /** 677 * Serializes the data according to the configured serializer. 678 * 679 * @param mixed $value 680 * @return string 681 */ 682 private function serialize($value) { 683 switch ($this->serializer) { 684 case Redis::SERIALIZER_NONE: 685 return $value; 686 case Redis::SERIALIZER_PHP: 687 return serialize($value); 688 case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: 689 return igbinary_serialize($value); 690 default: 691 debugging("Invalid serializer: {$this->serializer}"); 692 return $value; 693 } 694 } 695 696 /** 697 * Unserializes the data according to the configured serializer 698 * 699 * @param string $value 700 * @return mixed 701 */ 702 private function unserialize($value) { 703 switch ($this->serializer) { 704 case Redis::SERIALIZER_NONE: 705 return $value; 706 case Redis::SERIALIZER_PHP: 707 return unserialize($value); 708 case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: 709 return igbinary_unserialize($value); 710 default: 711 debugging("Invalid serializer: {$this->serializer}"); 712 return $value; 713 } 714 } 715 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body