Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 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  }