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