Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 * @var string Suffix used on key name (for hash) to store the TTL sorted list 58 */ 59 const TTL_SUFFIX = '_ttl'; 60 61 /** 62 * @var int Number of items to delete from cache in one batch when expiring old TTL data. 63 */ 64 const TTL_EXPIRE_BATCH = 10000; 65 66 /** 67 * Name of this store. 68 * 69 * @var string 70 */ 71 protected $name; 72 73 /** 74 * The definition hash, used for hash key 75 * 76 * @var string 77 */ 78 protected $hash; 79 80 /** 81 * Flag for readiness! 82 * 83 * @var boolean 84 */ 85 protected $isready = false; 86 87 /** 88 * Cache definition for this store. 89 * 90 * @var cache_definition 91 */ 92 protected $definition = null; 93 94 /** 95 * Connection to Redis for this store. 96 * 97 * @var Redis 98 */ 99 protected $redis; 100 101 /** 102 * Serializer for this store. 103 * 104 * @var int 105 */ 106 protected $serializer = Redis::SERIALIZER_PHP; 107 108 /** 109 * Compressor for this store. 110 * 111 * @var int 112 */ 113 protected $compressor = self::COMPRESSOR_NONE; 114 115 /** 116 * Bytes read or written by last call to set()/get() or set_many()/get_many(). 117 * 118 * @var int 119 */ 120 protected $lastiobytes = 0; 121 122 /** @var int Maximum number of seconds to wait for a lock before giving up. */ 123 protected $lockwait = 60; 124 125 /** @var int Timeout before lock is automatically released (in case of crashes) */ 126 protected $locktimeout = 600; 127 128 /** @var ?array Array of current locks, or null if we haven't registered shutdown function */ 129 protected $currentlocks = null; 130 131 /** 132 * Determines if the requirements for this type of store are met. 133 * 134 * @return bool 135 */ 136 public static function are_requirements_met() { 137 return class_exists('Redis'); 138 } 139 140 /** 141 * Determines if this type of store supports a given mode. 142 * 143 * @param int $mode 144 * @return bool 145 */ 146 public static function is_supported_mode($mode) { 147 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); 148 } 149 150 /** 151 * Get the features of this type of cache store. 152 * 153 * @param array $configuration 154 * @return int 155 */ 156 public static function get_supported_features(array $configuration = array()) { 157 // Although this plugin now supports TTL I did not add SUPPORTS_NATIVE_TTL here, because 158 // doing so would cause Moodle to stop adding a 'TTL wrapper' to data items which enforces 159 // the precise specified TTL. Unless the scheduled task is set to run rather frequently, 160 // this could cause change in behaviour. Maybe later this should be reconsidered... 161 return self::SUPPORTS_DATA_GUARANTEE + self::DEREFERENCES_OBJECTS + self::IS_SEARCHABLE; 162 } 163 164 /** 165 * Get the supported modes of this type of cache store. 166 * 167 * @param array $configuration 168 * @return int 169 */ 170 public static function get_supported_modes(array $configuration = array()) { 171 return self::MODE_APPLICATION + self::MODE_SESSION; 172 } 173 174 /** 175 * Constructs an instance of this type of store. 176 * 177 * @param string $name 178 * @param array $configuration 179 */ 180 public function __construct($name, array $configuration = array()) { 181 $this->name = $name; 182 183 if (!array_key_exists('server', $configuration) || empty($configuration['server'])) { 184 return; 185 } 186 if (array_key_exists('serializer', $configuration)) { 187 $this->serializer = (int)$configuration['serializer']; 188 } 189 if (array_key_exists('compressor', $configuration)) { 190 $this->compressor = (int)$configuration['compressor']; 191 } 192 if (array_key_exists('lockwait', $configuration)) { 193 $this->lockwait = (int)$configuration['lockwait']; 194 } 195 if (array_key_exists('locktimeout', $configuration)) { 196 $this->locktimeout = (int)$configuration['locktimeout']; 197 } 198 $this->redis = $this->new_redis($configuration); 199 } 200 201 /** 202 * Create a new Redis instance and 203 * connect to the server. 204 * 205 * @param array $configuration The server configuration 206 * @return Redis 207 */ 208 protected function new_redis(array $configuration): \Redis { 209 global $CFG; 210 211 $redis = new Redis(); 212 213 $server = $configuration['server']; 214 $encrypt = (bool) ($configuration['encryption'] ?? false); 215 $password = !empty($configuration['password']) ? $configuration['password'] : ''; 216 $prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : ''; 217 // Check if it isn't a Unix socket to set default port. 218 $port = null; 219 $opts = []; 220 if ($server[0] === '/') { 221 $port = 0; 222 } else { 223 $port = 6379; // No Unix socket so set default port. 224 if (strpos($server, ':')) { // Check for custom port. 225 list($server, $port) = explode(':', $server); 226 } 227 228 // We can encrypt if we aren't unix socket. 229 if ($encrypt) { 230 $server = 'tls://' . $server; 231 if (empty($configuration['cafile'])) { 232 $sslopts = [ 233 'verify_peer' => false, 234 'verify_peer_name' => false, 235 ]; 236 } else { 237 $sslopts = ['cafile' => $configuration['cafile']]; 238 } 239 $opts['stream'] = $sslopts; 240 } 241 } 242 243 try { 244 if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) { 245 246 if (!empty($password)) { 247 $redis->auth($password); 248 } 249 // If using compressor, serialisation will be done at cachestore level, not php-redis. 250 if ($this->compressor == self::COMPRESSOR_NONE) { 251 $redis->setOption(Redis::OPT_SERIALIZER, $this->serializer); 252 } 253 if (!empty($prefix)) { 254 $redis->setOption(Redis::OPT_PREFIX, $prefix); 255 } 256 if ($encrypt && !$redis->ping()) { 257 /* 258 * In case of a TLS connection, if phpredis client does not 259 * communicate immediately with the server the connection hangs. 260 * See https://github.com/phpredis/phpredis/issues/2332 . 261 */ 262 throw new \RedisException("Ping failed"); 263 } 264 $this->isready = true; 265 } else { 266 $this->isready = false; 267 } 268 } catch (\RedisException $e) { 269 debugging("redis $server: $e", DEBUG_NORMAL); 270 $this->isready = false; 271 } 272 273 return $redis; 274 } 275 276 /** 277 * See if we can ping Redis server 278 * 279 * @param Redis $redis 280 * @return bool 281 */ 282 protected function ping(Redis $redis) { 283 try { 284 if ($redis->ping() === false) { 285 return false; 286 } 287 } catch (Exception $e) { 288 return false; 289 } 290 return true; 291 } 292 293 /** 294 * Get the name of the store. 295 * 296 * @return string 297 */ 298 public function my_name() { 299 return $this->name; 300 } 301 302 /** 303 * Initialize the store. 304 * 305 * @param cache_definition $definition 306 * @return bool 307 */ 308 public function initialise(cache_definition $definition) { 309 $this->definition = $definition; 310 $this->hash = $definition->generate_definition_hash(); 311 return true; 312 } 313 314 /** 315 * Determine if the store is initialized. 316 * 317 * @return bool 318 */ 319 public function is_initialised() { 320 return ($this->definition !== null); 321 } 322 323 /** 324 * Determine if the store is ready for use. 325 * 326 * @return bool 327 */ 328 public function is_ready() { 329 return $this->isready; 330 } 331 332 /** 333 * Get the value associated with a given key. 334 * 335 * @param string $key The key to get the value of. 336 * @return mixed The value of the key, or false if there is no value associated with the key. 337 */ 338 public function get($key) { 339 $value = $this->redis->hGet($this->hash, $key); 340 341 if ($this->compressor == self::COMPRESSOR_NONE) { 342 return $value; 343 } 344 345 // When using compression, values are always strings, so strlen will work. 346 $this->lastiobytes = strlen($value); 347 348 return $this->uncompress($value); 349 } 350 351 /** 352 * Get the values associated with a list of keys. 353 * 354 * @param array $keys The keys to get the values of. 355 * @return array An array of the values of the given keys. 356 */ 357 public function get_many($keys) { 358 $values = $this->redis->hMGet($this->hash, $keys); 359 360 if ($this->compressor == self::COMPRESSOR_NONE) { 361 return $values; 362 } 363 364 $this->lastiobytes = 0; 365 foreach ($values as &$value) { 366 $this->lastiobytes += strlen($value); 367 $value = $this->uncompress($value); 368 } 369 370 return $values; 371 } 372 373 /** 374 * Gets the number of bytes read from or written to cache as a result of the last action. 375 * 376 * If compression is not enabled, this function always returns IO_BYTES_NOT_SUPPORTED. The reason is that 377 * when compression is not enabled, data sent to the cache is not serialized, and we would 378 * need to serialize it to compute the size, which would have a significant performance cost. 379 * 380 * @return int Bytes read or written 381 * @since Moodle 4.0 382 */ 383 public function get_last_io_bytes(): int { 384 if ($this->compressor != self::COMPRESSOR_NONE) { 385 return $this->lastiobytes; 386 } else { 387 // Not supported unless compression is on. 388 return parent::get_last_io_bytes(); 389 } 390 } 391 392 /** 393 * Set the value of a key. 394 * 395 * @param string $key The key to set the value of. 396 * @param mixed $value The value. 397 * @return bool True if the operation succeeded, false otherwise. 398 */ 399 public function set($key, $value) { 400 if ($this->compressor != self::COMPRESSOR_NONE) { 401 $value = $this->compress($value); 402 $this->lastiobytes = strlen($value); 403 } 404 405 if ($this->redis->hSet($this->hash, $key, $value) === false) { 406 return false; 407 } 408 if ($this->definition->get_ttl()) { 409 // When TTL is enabled, we also store the key name in a list sorted by the current time. 410 $this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], self::get_time(), $key); 411 // The return value to the zAdd function never indicates whether the operation succeeded 412 // (it returns zero when there was no error if the item is already in the list) so we 413 // ignore it. 414 } 415 return true; 416 } 417 418 /** 419 * Set the values of many keys. 420 * 421 * @param array $keyvaluearray An array of key/value pairs. Each item in the array is an associative array 422 * with two keys, 'key' and 'value'. 423 * @return int The number of key/value pairs successfuly set. 424 */ 425 public function set_many(array $keyvaluearray) { 426 $pairs = []; 427 $usettl = false; 428 if ($this->definition->get_ttl()) { 429 $usettl = true; 430 $ttlparams = []; 431 $now = self::get_time(); 432 } 433 434 $this->lastiobytes = 0; 435 foreach ($keyvaluearray as $pair) { 436 $key = $pair['key']; 437 if ($this->compressor != self::COMPRESSOR_NONE) { 438 $pairs[$key] = $this->compress($pair['value']); 439 $this->lastiobytes += strlen($pairs[$key]); 440 } else { 441 $pairs[$key] = $pair['value']; 442 } 443 if ($usettl) { 444 // When TTL is enabled, we also store the key names in a list sorted by the current 445 // time. 446 $ttlparams[] = $now; 447 $ttlparams[] = $key; 448 } 449 } 450 if ($usettl) { 451 // Store all the key values with current time. 452 $this->redis->zAdd($this->hash . self::TTL_SUFFIX, [], ...$ttlparams); 453 // The return value to the zAdd function never indicates whether the operation succeeded 454 // (it returns zero when there was no error if the item is already in the list) so we 455 // ignore it. 456 } 457 if ($this->redis->hMSet($this->hash, $pairs)) { 458 return count($pairs); 459 } 460 return 0; 461 } 462 463 /** 464 * Delete the given key. 465 * 466 * @param string $key The key to delete. 467 * @return bool True if the delete operation succeeds, false otherwise. 468 */ 469 public function delete($key) { 470 $ok = true; 471 if (!$this->redis->hDel($this->hash, $key)) { 472 $ok = false; 473 } 474 if ($this->definition->get_ttl()) { 475 // When TTL is enabled, also remove the key from the TTL list. 476 $this->redis->zRem($this->hash . self::TTL_SUFFIX, $key); 477 } 478 return $ok; 479 } 480 481 /** 482 * Delete many keys. 483 * 484 * @param array $keys The keys to delete. 485 * @return int The number of keys successfully deleted. 486 */ 487 public function delete_many(array $keys) { 488 // If there are no keys to delete, do nothing. 489 if (!$keys) { 490 return 0; 491 } 492 $count = $this->redis->hDel($this->hash, ...$keys); 493 if ($this->definition->get_ttl()) { 494 // When TTL is enabled, also remove the keys from the TTL list. 495 $this->redis->zRem($this->hash . self::TTL_SUFFIX, ...$keys); 496 } 497 return $count; 498 } 499 500 /** 501 * Purges all keys from the store. 502 * 503 * @return bool 504 */ 505 public function purge() { 506 if ($this->definition->get_ttl()) { 507 // Purge the TTL list as well. 508 $this->redis->del($this->hash . self::TTL_SUFFIX); 509 // According to documentation, there is no error return for the 'del' command (it 510 // only returns the number of keys deleted, which could be 0 or 1 in this case) so we 511 // do not need to check the return value. 512 } 513 return ($this->redis->del($this->hash) !== false); 514 } 515 516 /** 517 * Cleans up after an instance of the store. 518 */ 519 public function instance_deleted() { 520 $this->redis->close(); 521 unset($this->redis); 522 } 523 524 /** 525 * Determines if the store has a given key. 526 * 527 * @see cache_is_key_aware 528 * @param string $key The key to check for. 529 * @return bool True if the key exists, false if it does not. 530 */ 531 public function has($key) { 532 return !empty($this->redis->hExists($this->hash, $key)); 533 } 534 535 /** 536 * Determines if the store has any of the keys in a list. 537 * 538 * @see cache_is_key_aware 539 * @param array $keys The keys to check for. 540 * @return bool True if any of the keys are found, false none of the keys are found. 541 */ 542 public function has_any(array $keys) { 543 foreach ($keys as $key) { 544 if ($this->has($key)) { 545 return true; 546 } 547 } 548 return false; 549 } 550 551 /** 552 * Determines if the store has all of the keys in a list. 553 * 554 * @see cache_is_key_aware 555 * @param array $keys The keys to check for. 556 * @return bool True if all of the keys are found, false otherwise. 557 */ 558 public function has_all(array $keys) { 559 foreach ($keys as $key) { 560 if (!$this->has($key)) { 561 return false; 562 } 563 } 564 return true; 565 } 566 567 /** 568 * Tries to acquire a lock with a given name. 569 * 570 * @see cache_is_lockable 571 * @param string $key Name of the lock to acquire. 572 * @param string $ownerid Information to identify owner of lock if acquired. 573 * @return bool True if the lock was acquired, false if it was not. 574 */ 575 public function acquire_lock($key, $ownerid) { 576 $timelimit = time() + $this->lockwait; 577 do { 578 // If the key doesn't already exist, grab it and return true. 579 if ($this->redis->setnx($key, $ownerid)) { 580 // Ensure Redis deletes the key after a bit in case something goes wrong. 581 $this->redis->expire($key, $this->locktimeout); 582 // If we haven't got it already, better register a shutdown function. 583 if ($this->currentlocks === null) { 584 core_shutdown_manager::register_function([$this, 'shutdown_release_locks']); 585 $this->currentlocks = []; 586 } 587 $this->currentlocks[$key] = $ownerid; 588 return true; 589 } 590 // Wait 1 second then retry. 591 sleep(1); 592 } while (time() < $timelimit); 593 return false; 594 } 595 596 /** 597 * Releases any locks when the system shuts down, in case there is a crash or somebody forgets 598 * to use 'try-finally'. 599 * 600 * Do not call this function manually (except from unit test). 601 */ 602 public function shutdown_release_locks() { 603 foreach ($this->currentlocks as $key => $ownerid) { 604 debugging('Automatically releasing Redis cache lock: ' . $key . ' (' . $ownerid . 605 ') - did somebody forget to call release_lock()?', DEBUG_DEVELOPER); 606 $this->release_lock($key, $ownerid); 607 } 608 } 609 610 /** 611 * Checks a lock with a given name and owner information. 612 * 613 * @see cache_is_lockable 614 * @param string $key Name of the lock to check. 615 * @param string $ownerid Owner information to check existing lock against. 616 * @return mixed True if the lock exists and the owner information matches, null if the lock does not 617 * exist, and false otherwise. 618 */ 619 public function check_lock_state($key, $ownerid) { 620 $result = $this->redis->get($key); 621 if ($result === (string)$ownerid) { 622 return true; 623 } 624 if ($result === false) { 625 return null; 626 } 627 return false; 628 } 629 630 /** 631 * Finds all of the keys being used by this cache store instance. 632 * 633 * @return array of all keys in the hash as a numbered array. 634 */ 635 public function find_all() { 636 return $this->redis->hKeys($this->hash); 637 } 638 639 /** 640 * Finds all of the keys whose keys start with the given prefix. 641 * 642 * @param string $prefix 643 * 644 * @return array List of keys that match this prefix. 645 */ 646 public function find_by_prefix($prefix) { 647 $return = []; 648 foreach ($this->find_all() as $key) { 649 if (strpos($key, $prefix) === 0) { 650 $return[] = $key; 651 } 652 } 653 return $return; 654 } 655 656 /** 657 * Releases a given lock if the owner information matches. 658 * 659 * @see cache_is_lockable 660 * @param string $key Name of the lock to release. 661 * @param string $ownerid Owner information to use. 662 * @return bool True if the lock is released, false if it is not. 663 */ 664 public function release_lock($key, $ownerid) { 665 if ($this->check_lock_state($key, $ownerid)) { 666 unset($this->currentlocks[$key]); 667 return ($this->redis->del($key) !== false); 668 } 669 return false; 670 } 671 672 /** 673 * Runs TTL expiry process for this cache. 674 * 675 * This is not part of the standard cache API and is intended for use by the scheduled task 676 * \cachestore_redis\ttl. 677 * 678 * @return array Various keys with information about how the expiry went 679 */ 680 public function expire_ttl(): array { 681 $ttl = $this->definition->get_ttl(); 682 if (!$ttl) { 683 throw new \coding_exception('Cache definition ' . $this->definition->get_id() . ' does not use TTL'); 684 } 685 $limit = self::get_time() - $ttl; 686 $count = 0; 687 $batches = 0; 688 $timebefore = microtime(true); 689 $memorybefore = $this->store_total_size(); 690 do { 691 $keys = $this->redis->zRangeByScore($this->hash . self::TTL_SUFFIX, 0, $limit, 692 ['limit' => [0, self::TTL_EXPIRE_BATCH]]); 693 $this->delete_many($keys); 694 $count += count($keys); 695 $batches++; 696 } while (count($keys) === self::TTL_EXPIRE_BATCH); 697 $memoryafter = $this->store_total_size(); 698 $timeafter = microtime(true); 699 700 $result = ['keys' => $count, 'batches' => $batches, 'time' => $timeafter - $timebefore]; 701 if ($memorybefore !== null) { 702 $result['memory'] = $memorybefore - $memoryafter; 703 } 704 return $result; 705 } 706 707 /** 708 * Gets the current time for TTL functionality. This wrapper makes it easier to unit-test 709 * the TTL behaviour. 710 * 711 * @return int Current time 712 */ 713 protected static function get_time(): int { 714 global $CFG; 715 if (PHPUNIT_TEST && !empty($CFG->phpunit_cachestore_redis_time)) { 716 return $CFG->phpunit_cachestore_redis_time; 717 } 718 return time(); 719 } 720 721 /** 722 * Sets the current time (within unit test) for TTL functionality. 723 * 724 * This setting is stored in $CFG so will be automatically reset if you use resetAfterTest. 725 * 726 * @param int $time Current time (set 0 to start using real time). 727 */ 728 public static function set_phpunit_time(int $time = 0): void { 729 global $CFG; 730 if (!PHPUNIT_TEST) { 731 throw new \coding_exception('Function only available during unit test'); 732 } 733 if ($time) { 734 $CFG->phpunit_cachestore_redis_time = $time; 735 } else { 736 unset($CFG->phpunit_cachestore_redis_time); 737 } 738 } 739 740 /** 741 * Estimates the stored size, taking into account whether compression is turned on. 742 * 743 * @param mixed $key Key name 744 * @param mixed $value Value 745 * @return int Approximate stored size 746 */ 747 public function estimate_stored_size($key, $value): int { 748 if ($this->compressor == self::COMPRESSOR_NONE) { 749 // If uncompressed, use default estimate. 750 return parent::estimate_stored_size($key, $value); 751 } else { 752 // If compressed, compress value. 753 return strlen($this->serialize($key)) + strlen($this->compress($value)); 754 } 755 } 756 757 /** 758 * Gets Redis reported memory usage. 759 * 760 * @return int|null Memory used by Redis or null if we don't know 761 */ 762 public function store_total_size(): ?int { 763 try { 764 $details = $this->redis->info('MEMORY'); 765 } catch (\RedisException $e) { 766 return null; 767 } 768 if (empty($details['used_memory'])) { 769 return null; 770 } else { 771 return (int)$details['used_memory']; 772 } 773 } 774 775 /** 776 * Creates a configuration array from given 'add instance' form data. 777 * 778 * @see cache_is_configurable 779 * @param stdClass $data 780 * @return array 781 */ 782 public static function config_get_configuration_array($data) { 783 return array( 784 'server' => $data->server, 785 'prefix' => $data->prefix, 786 'password' => $data->password, 787 'serializer' => $data->serializer, 788 'compressor' => $data->compressor, 789 'encryption' => $data->encryption, 790 'cafile' => $data->cafile, 791 ); 792 } 793 794 /** 795 * Sets form data from a configuration array. 796 * 797 * @see cache_is_configurable 798 * @param moodleform $editform 799 * @param array $config 800 */ 801 public static function config_set_edit_form_data(moodleform $editform, array $config) { 802 $data = array(); 803 $data['server'] = $config['server']; 804 $data['prefix'] = !empty($config['prefix']) ? $config['prefix'] : ''; 805 $data['password'] = !empty($config['password']) ? $config['password'] : ''; 806 if (!empty($config['serializer'])) { 807 $data['serializer'] = $config['serializer']; 808 } 809 if (!empty($config['compressor'])) { 810 $data['compressor'] = $config['compressor']; 811 } 812 if (!empty($config['encryption'])) { 813 $data['encryption'] = $config['encryption']; 814 } 815 if (!empty($config['cafile'])) { 816 $data['cafile'] = $config['cafile']; 817 } 818 $editform->set_data($data); 819 } 820 821 822 /** 823 * Creates an instance of the store for testing. 824 * 825 * @param cache_definition $definition 826 * @return mixed An instance of the store, or false if an instance cannot be created. 827 */ 828 public static function initialise_test_instance(cache_definition $definition) { 829 if (!self::are_requirements_met()) { 830 return false; 831 } 832 $config = get_config('cachestore_redis'); 833 if (empty($config->test_server)) { 834 return false; 835 } 836 $configuration = array('server' => $config->test_server); 837 if (!empty($config->test_serializer)) { 838 $configuration['serializer'] = $config->test_serializer; 839 } 840 if (!empty($config->test_password)) { 841 $configuration['password'] = $config->test_password; 842 } 843 if (!empty($config->test_encryption)) { 844 $configuration['encryption'] = $config->test_encryption; 845 } 846 if (!empty($config->test_cafile)) { 847 $configuration['cafile'] = $config->test_cafile; 848 } 849 // Make it possible to test TTL performance by hacking a copy of the cache definition. 850 if (!empty($config->test_ttl)) { 851 $definition = clone $definition; 852 $property = (new ReflectionClass($definition))->getProperty('ttl'); 853 $property->setAccessible(true); 854 $property->setValue($definition, 999); 855 } 856 $cache = new cachestore_redis('Redis test', $configuration); 857 $cache->initialise($definition); 858 859 return $cache; 860 } 861 862 /** 863 * Return configuration to use when unit testing. 864 * 865 * @return array 866 */ 867 public static function unit_test_configuration() { 868 global $DB; 869 870 if (!self::are_requirements_met() || !self::ready_to_be_used_for_testing()) { 871 throw new moodle_exception('TEST_CACHESTORE_REDIS_TESTSERVERS not configured, unable to create test configuration'); 872 } 873 874 return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS, 875 'prefix' => $DB->get_prefix(), 876 'encryption' => defined('TEST_CACHESTORE_REDIS_ENCRYPT') && TEST_CACHESTORE_REDIS_ENCRYPT, 877 ]; 878 } 879 880 /** 881 * Returns true if this cache store instance is both suitable for testing, and ready for testing. 882 * 883 * When TEST_CACHESTORE_REDIS_TESTSERVERS is set, then we are ready to be use d for testing. 884 * 885 * @return bool 886 */ 887 public static function ready_to_be_used_for_testing() { 888 return defined('TEST_CACHESTORE_REDIS_TESTSERVERS'); 889 } 890 891 /** 892 * Gets an array of options to use as the serialiser. 893 * @return array 894 */ 895 public static function config_get_serializer_options() { 896 $options = array( 897 Redis::SERIALIZER_PHP => get_string('serializer_php', 'cachestore_redis') 898 ); 899 900 if (defined('Redis::SERIALIZER_IGBINARY')) { 901 $options[Redis::SERIALIZER_IGBINARY] = get_string('serializer_igbinary', 'cachestore_redis'); 902 } 903 return $options; 904 } 905 906 /** 907 * Gets an array of options to use as the compressor. 908 * 909 * @return array 910 */ 911 public static function config_get_compressor_options() { 912 $arr = [ 913 self::COMPRESSOR_NONE => get_string('compressor_none', 'cachestore_redis'), 914 self::COMPRESSOR_PHP_GZIP => get_string('compressor_php_gzip', 'cachestore_redis'), 915 ]; 916 917 // Check if the Zstandard PHP extension is installed. 918 if (extension_loaded('zstd')) { 919 $arr[self::COMPRESSOR_PHP_ZSTD] = get_string('compressor_php_zstd', 'cachestore_redis'); 920 } 921 922 return $arr; 923 } 924 925 /** 926 * Compress the given value, serializing it first. 927 * 928 * @param mixed $value 929 * @return string 930 */ 931 private function compress($value) { 932 $value = $this->serialize($value); 933 934 switch ($this->compressor) { 935 case self::COMPRESSOR_NONE: 936 return $value; 937 938 case self::COMPRESSOR_PHP_GZIP: 939 return gzencode($value); 940 941 case self::COMPRESSOR_PHP_ZSTD: 942 return zstd_compress($value); 943 944 default: 945 debugging("Invalid compressor: {$this->compressor}"); 946 return $value; 947 } 948 } 949 950 /** 951 * Uncompresses (deflates) the data, unserialising it afterwards. 952 * 953 * @param string $value 954 * @return mixed 955 */ 956 private function uncompress($value) { 957 if ($value === false) { 958 return false; 959 } 960 961 switch ($this->compressor) { 962 case self::COMPRESSOR_NONE: 963 break; 964 case self::COMPRESSOR_PHP_GZIP: 965 $value = gzdecode($value); 966 break; 967 case self::COMPRESSOR_PHP_ZSTD: 968 $value = zstd_uncompress($value); 969 break; 970 default: 971 debugging("Invalid compressor: {$this->compressor}"); 972 } 973 974 return $this->unserialize($value); 975 } 976 977 /** 978 * Serializes the data according to the configured serializer. 979 * 980 * @param mixed $value 981 * @return string 982 */ 983 private function serialize($value) { 984 switch ($this->serializer) { 985 case Redis::SERIALIZER_NONE: 986 return $value; 987 case Redis::SERIALIZER_PHP: 988 return serialize($value); 989 case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: 990 return igbinary_serialize($value); 991 default: 992 debugging("Invalid serializer: {$this->serializer}"); 993 return $value; 994 } 995 } 996 997 /** 998 * Unserializes the data according to the configured serializer 999 * 1000 * @param string $value 1001 * @return mixed 1002 */ 1003 private function unserialize($value) { 1004 switch ($this->serializer) { 1005 case Redis::SERIALIZER_NONE: 1006 return $value; 1007 case Redis::SERIALIZER_PHP: 1008 return unserialize($value); 1009 case defined('Redis::SERIALIZER_IGBINARY') && Redis::SERIALIZER_IGBINARY: 1010 return igbinary_unserialize($value); 1011 default: 1012 debugging("Invalid serializer: {$this->serializer}"); 1013 return $value; 1014 } 1015 } 1016 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body