Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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