Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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   * The library file for the memcached cache store.
  19   *
  20   * This file is part of the memcached cache store, it contains the API for interacting with an instance of the store.
  21   *
  22   * @package    cachestore_memcached
  23   * @copyright  2012 Sam Hemelryk
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * The memcached store.
  31   *
  32   * (Not to be confused with the memcache store)
  33   *
  34   * Configuration options:
  35   *      servers:        string: host:port:weight , ...
  36   *      compression:    true, false
  37   *      serialiser:     SERIALIZER_PHP, SERIALIZER_JSON, SERIALIZER_IGBINARY
  38   *      prefix:         string: defaults to instance name
  39   *      hashmethod:     HASH_DEFAULT, HASH_MD5, HASH_CRC, HASH_FNV1_64, HASH_FNV1A_64, HASH_FNV1_32,
  40   *                      HASH_FNV1A_32, HASH_HSIEH, HASH_MURMUR
  41   *      bufferwrites:   true, false
  42   *
  43   * @copyright  2012 Sam Hemelryk
  44   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class cachestore_memcached extends cache_store implements cache_is_configurable {
  47  
  48      /**
  49       * The minimum required version of memcached in order to use this store.
  50       */
  51      const REQUIRED_VERSION = '2.0.0';
  52  
  53      /**
  54       * The name of the store
  55       * @var store
  56       */
  57      protected $name;
  58  
  59      /**
  60       * The memcached connection
  61       * @var Memcached
  62       */
  63      protected $connection;
  64  
  65      /**
  66       * An array of servers to use during connection
  67       * @var array
  68       */
  69      protected $servers = array();
  70  
  71      /**
  72       * The options used when establishing the connection
  73       * @var array
  74       */
  75      protected $options = array();
  76  
  77      /**
  78       * True when this instance is ready to be initialised.
  79       * @var bool
  80       */
  81      protected $isready = false;
  82  
  83      /**
  84       * Set to true when this store instance has been initialised.
  85       * @var bool
  86       */
  87      protected $isinitialised = false;
  88  
  89      /**
  90       * The cache definition this store was initialised with.
  91       * @var cache_definition
  92       */
  93      protected $definition;
  94  
  95      /**
  96       * Set to true when this store is clustered.
  97       * @var bool
  98       */
  99      protected $clustered = false;
 100  
 101      /**
 102       * Array of servers to set when in clustered mode.
 103       * @var array
 104       */
 105      protected $setservers = array();
 106  
 107      /**
 108       * The an array of memcache connections for the set servers, once established.
 109       * @var array
 110       */
 111      protected $setconnections = array();
 112  
 113      /**
 114       * The prefix to use on all keys.
 115       * @var string
 116       */
 117      protected $prefix = '';
 118  
 119      /**
 120       * True if Memcached::deleteMulti can be used, false otherwise.
 121       * This required extension version 2.0.0 or greater.
 122       * @var bool
 123       */
 124      protected $candeletemulti = false;
 125  
 126      /**
 127       * True if the memcached server is shared, false otherwise.
 128       * This required extension version 2.0.0 or greater.
 129       * @var bool
 130       */
 131      protected $isshared = false;
 132  
 133      /**
 134       * Constructs the store instance.
 135       *
 136       * Noting that this function is not an initialisation. It is used to prepare the store for use.
 137       * The store will be initialised when required and will be provided with a cache_definition at that time.
 138       *
 139       * @param string $name
 140       * @param array $configuration
 141       */
 142      public function __construct($name, array $configuration = array()) {
 143          $this->name = $name;
 144          if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
 145              // Nothing configured.
 146              return;
 147          }
 148          if (!is_array($configuration['servers'])) {
 149              $configuration['servers'] = array($configuration['servers']);
 150          }
 151  
 152          $compression = array_key_exists('compression', $configuration) ? (bool)$configuration['compression'] : true;
 153          if (array_key_exists('serialiser', $configuration)) {
 154              $serialiser = (int)$configuration['serialiser'];
 155          } else {
 156              $serialiser = Memcached::SERIALIZER_PHP;
 157          }
 158          $prefix = (!empty($configuration['prefix'])) ? (string)$configuration['prefix'] : crc32($name);
 159          $hashmethod = (array_key_exists('hash', $configuration)) ? (int)$configuration['hash'] : Memcached::HASH_DEFAULT;
 160          $bufferwrites = array_key_exists('bufferwrites', $configuration) ? (bool)$configuration['bufferwrites'] : false;
 161  
 162          foreach ($configuration['servers'] as $server) {
 163              if (!is_array($server)) {
 164                  $server = explode(':', $server, 3);
 165              }
 166              if (!array_key_exists(1, $server)) {
 167                  $server[1] = 11211;
 168                  $server[2] = 100;
 169              } else if (!array_key_exists(2, $server)) {
 170                  $server[2] = 100;
 171              }
 172              $this->servers[] = $server;
 173          }
 174  
 175          $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
 176  
 177          if ($this->clustered) {
 178              if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
 179                  // Can't setup clustering without set servers.
 180                  return;
 181              }
 182              if (count($this->servers) !== 1) {
 183                  // Can only setup cluster with exactly 1 get server.
 184                  return;
 185              }
 186              foreach ($configuration['setservers'] as $server) {
 187                  // We do not use weights (3rd part) on these servers.
 188                  if (!is_array($server)) {
 189                      $server = explode(':', $server, 3);
 190                  }
 191                  if (!array_key_exists(1, $server)) {
 192                      $server[1] = 11211;
 193                  }
 194                  $this->setservers[] = $server;
 195              }
 196          }
 197  
 198          $this->options[Memcached::OPT_COMPRESSION] = $compression;
 199          $this->options[Memcached::OPT_SERIALIZER] = $serialiser;
 200          $this->options[Memcached::OPT_PREFIX_KEY] = $this->prefix = (string)$prefix;
 201          $this->options[Memcached::OPT_HASH] = $hashmethod;
 202          $this->options[Memcached::OPT_BUFFER_WRITES] = $bufferwrites;
 203  
 204          $this->connection = new Memcached(crc32($this->name));
 205          $servers = $this->connection->getServerList();
 206          if (empty($servers)) {
 207              foreach ($this->options as $key => $value) {
 208                  $this->connection->setOption($key, $value);
 209              }
 210              $this->connection->addServers($this->servers);
 211          }
 212  
 213          if ($this->clustered) {
 214              foreach ($this->setservers as $setserver) {
 215                  // Since we will have a number of them with the same name, append server and port.
 216                  $connection = new Memcached(crc32($this->name.$setserver[0].$setserver[1]));
 217                  foreach ($this->options as $key => $value) {
 218                      $connection->setOption($key, $value);
 219                  }
 220                  $connection->addServer($setserver[0], $setserver[1]);
 221                  $this->setconnections[] = $connection;
 222              }
 223          }
 224  
 225          if (isset($configuration['isshared'])) {
 226              $this->isshared = $configuration['isshared'];
 227          }
 228  
 229          $version = phpversion('memcached');
 230          $this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
 231  
 232          $this->isready = $this->is_connection_ready();
 233      }
 234  
 235      /**
 236       * Confirm whether the connection is ready and usable.
 237       *
 238       * @return boolean
 239       */
 240      public function is_connection_ready() {
 241          if (!@$this->connection->set("ping", 'ping', 1)) {
 242              // Test the connection to the server.
 243              return false;
 244          }
 245  
 246          if ($this->isshared) {
 247              // There is a bug in libmemcached which means that it is not possible to purge the cache in a shared cache
 248              // configuration.
 249              // This issue currently affects:
 250              // - memcached 1.4.23+ with php-memcached <= 2.2.0
 251              // The following combinations are not affected:
 252              // - memcached <= 1.4.22 with any version of php-memcached
 253              // - any version of memcached with php-memcached >= 3.0.1
 254  
 255  
 256              // This check is cheapest as it does not involve connecting to the server at all.
 257              $safecombination = false;
 258              $extension = new ReflectionExtension('memcached');
 259              if ((version_compare($extension->getVersion(), '3.0.1') >= 0)) {
 260                  // This is php-memcached version >= 3.0.1 which is a safe combination.
 261                  $safecombination = true;
 262              }
 263  
 264              if (!$safecombination) {
 265                  $allsafe = true;
 266                  foreach ($this->connection->getVersion() as $version) {
 267                      $allsafe = $allsafe && (version_compare($version, '1.4.22') <= 0);
 268                  }
 269                  // All memcached servers connected are version <= 1.4.22 which is a safe combination.
 270                  $safecombination = $allsafe;
 271              }
 272  
 273              if (!$safecombination) {
 274                  // This is memcached 1.4.23+ and php-memcached < 3.0.1.
 275                  // The issue may have been resolved in a subsequent update to any of the three libraries.
 276                  // The only way to safely determine if the combination is safe is to call getAllKeys.
 277                  // A safe combination will return an array, whilst an affected combination will return false.
 278                  // This is the most expensive check.
 279                  if (!is_array($this->connection->getAllKeys())) {
 280                      return false;
 281                  }
 282              }
 283          }
 284  
 285          return true;
 286      }
 287  
 288      /**
 289       * Initialises the cache.
 290       *
 291       * Once this has been done the cache is all set to be used.
 292       *
 293       * @throws coding_exception if the instance has already been initialised.
 294       * @param cache_definition $definition
 295       */
 296      public function initialise(cache_definition $definition) {
 297          if ($this->is_initialised()) {
 298              throw new coding_exception('This memcached instance has already been initialised.');
 299          }
 300          $this->definition = $definition;
 301          $this->isinitialised = true;
 302      }
 303  
 304      /**
 305       * Returns true once this instance has been initialised.
 306       *
 307       * @return bool
 308       */
 309      public function is_initialised() {
 310          return ($this->isinitialised);
 311      }
 312  
 313      /**
 314       * Returns true if this store instance is ready to be used.
 315       * @return bool
 316       */
 317      public function is_ready() {
 318          return $this->isready;
 319      }
 320  
 321      /**
 322       * Returns true if the store requirements are met.
 323       *
 324       * @return bool
 325       */
 326      public static function are_requirements_met() {
 327          return extension_loaded('memcached') && class_exists('Memcached');
 328      }
 329  
 330      /**
 331       * Returns true if the given mode is supported by this store.
 332       *
 333       * @param int $mode One of cache_store::MODE_*
 334       * @return bool
 335       */
 336      public static function is_supported_mode($mode) {
 337          return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
 338      }
 339  
 340      /**
 341       * Returns the supported features as a combined int.
 342       *
 343       * @param array $configuration
 344       * @return int
 345       */
 346      public static function get_supported_features(array $configuration = array()) {
 347          return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
 348      }
 349  
 350      /**
 351       * Returns false as this store does not support multiple identifiers.
 352       * (This optional function is a performance optimisation; it must be
 353       * consistent with the value from get_supported_features.)
 354       *
 355       * @return bool False
 356       */
 357      public function supports_multiple_identifiers() {
 358          return false;
 359      }
 360  
 361      /**
 362       * Returns the supported modes as a combined int.
 363       *
 364       * @param array $configuration
 365       * @return int
 366       */
 367      public static function get_supported_modes(array $configuration = array()) {
 368          return self::MODE_APPLICATION;
 369      }
 370  
 371      /**
 372       * Retrieves an item from the cache store given its key.
 373       *
 374       * @param string $key The key to retrieve
 375       * @return mixed The data that was associated with the key, or false if the key did not exist.
 376       */
 377      public function get($key) {
 378          return $this->connection->get($key);
 379      }
 380  
 381      /**
 382       * Retrieves several items from the cache store in a single transaction.
 383       *
 384       * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
 385       *
 386       * @param array $keys The array of keys to retrieve
 387       * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
 388       *      be set to false.
 389       */
 390      public function get_many($keys) {
 391          $return = array();
 392          $result = $this->connection->getMulti($keys);
 393          if (!is_array($result)) {
 394              $result = array();
 395          }
 396          foreach ($keys as $key) {
 397              if (!array_key_exists($key, $result)) {
 398                  $return[$key] = false;
 399              } else {
 400                  $return[$key] = $result[$key];
 401              }
 402          }
 403          return $return;
 404      }
 405  
 406      /**
 407       * Sets an item in the cache given its key and data value.
 408       *
 409       * @param string $key The key to use.
 410       * @param mixed $data The data to set.
 411       * @return bool True if the operation was a success false otherwise.
 412       */
 413      public function set($key, $data) {
 414          if ($this->clustered) {
 415              $status = true;
 416              foreach ($this->setconnections as $connection) {
 417                  $status = $connection->set($key, $data, $this->definition->get_ttl()) && $status;
 418              }
 419              return $status;
 420          }
 421  
 422          return $this->connection->set($key, $data, $this->definition->get_ttl());
 423      }
 424  
 425      /**
 426       * Sets many items in the cache in a single transaction.
 427       *
 428       * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
 429       *      keys, 'key' and 'value'.
 430       * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
 431       *      sent ... if they care that is.
 432       */
 433      public function set_many(array $keyvaluearray) {
 434          $pairs = array();
 435          foreach ($keyvaluearray as $pair) {
 436              $pairs[$pair['key']] = $pair['value'];
 437          }
 438  
 439          $status = true;
 440          if ($this->clustered) {
 441              foreach ($this->setconnections as $connection) {
 442                  $status = $connection->setMulti($pairs, $this->definition->get_ttl()) && $status;
 443              }
 444          } else {
 445              $status = $this->connection->setMulti($pairs, $this->definition->get_ttl());
 446          }
 447  
 448          if ($status) {
 449              return count($keyvaluearray);
 450          }
 451          return 0;
 452      }
 453  
 454      /**
 455       * Deletes an item from the cache store.
 456       *
 457       * @param string $key The key to delete.
 458       * @return bool Returns true if the operation was a success, false otherwise.
 459       */
 460      public function delete($key) {
 461          if ($this->clustered) {
 462              $status = true;
 463              foreach ($this->setconnections as $connection) {
 464                  $status = $connection->delete($key) && $status;
 465              }
 466              return $status;
 467          }
 468  
 469          return $this->connection->delete($key);
 470      }
 471  
 472      /**
 473       * Deletes several keys from the cache in a single action.
 474       *
 475       * @param array $keys The keys to delete
 476       * @return int The number of items successfully deleted.
 477       */
 478      public function delete_many(array $keys) {
 479          if ($this->clustered) {
 480              // Get the minimum deleted from any of the connections.
 481              $count = count($keys);
 482              foreach ($this->setconnections as $connection) {
 483                  $count = min($this->delete_many_connection($connection, $keys), $count);
 484              }
 485              return $count;
 486          }
 487  
 488          return $this->delete_many_connection($this->connection, $keys);
 489      }
 490  
 491      /**
 492       * Deletes several keys from the cache in a single action for a specific connection.
 493       *
 494       * @param Memcached $connection The connection to work on.
 495       * @param array $keys The keys to delete
 496       * @return int The number of items successfully deleted.
 497       */
 498      protected function delete_many_connection(Memcached $connection, array $keys) {
 499          $count = 0;
 500          if ($this->candeletemulti) {
 501              // We can use deleteMulti, this is a bit faster yay!
 502              $result = $connection->deleteMulti($keys);
 503              foreach ($result as $key => $outcome) {
 504                  if ($outcome === true) {
 505                      $count++;
 506                  }
 507              }
 508              return $count;
 509          }
 510  
 511          // They are running an older version of the php memcached extension.
 512          foreach ($keys as $key) {
 513              if ($connection->delete($key)) {
 514                  $count++;
 515              }
 516          }
 517          return $count;
 518      }
 519  
 520      /**
 521       * Purges the cache deleting all items within it.
 522       *
 523       * @return boolean True on success. False otherwise.
 524       */
 525      public function purge() {
 526          if ($this->isready) {
 527              // Only use delete multi if we have the correct extension installed and if the memcached
 528              // server is shared (flushing the cache is quicker otherwise).
 529              $candeletemulti = ($this->candeletemulti && $this->isshared);
 530  
 531              if ($this->clustered) {
 532                  foreach ($this->setconnections as $connection) {
 533                      if ($candeletemulti) {
 534                          $keys = self::get_prefixed_keys($connection, $this->prefix);
 535                          $connection->deleteMulti($keys);
 536                      } else {
 537                          // Oh damn, this isn't multi-site safe.
 538                          $connection->flush();
 539                      }
 540                  }
 541              } else if ($candeletemulti) {
 542                  $keys = self::get_prefixed_keys($this->connection, $this->prefix);
 543                  $this->connection->deleteMulti($keys);
 544              } else {
 545                  // Oh damn, this isn't multi-site safe.
 546                  $this->connection->flush();
 547              }
 548          }
 549          // It never fails. Ever.
 550          return true;
 551      }
 552  
 553      /**
 554       * Returns all of the keys in the given connection that belong to this cache store instance.
 555       *
 556       * Requires php memcached extension version 2.0.0 or greater.
 557       *
 558       * @param Memcached $connection
 559       * @param string $prefix
 560       * @return array
 561       */
 562      protected static function get_prefixed_keys(Memcached $connection, $prefix) {
 563          $connkeys = $connection->getAllKeys();
 564          if (empty($connkeys)) {
 565              return array();
 566          }
 567  
 568          $keys = array();
 569          $start = strlen($prefix);
 570          foreach ($connkeys as $key) {
 571              if (strpos($key, $prefix) === 0) {
 572                  $keys[] = substr($key, $start);
 573              }
 574          }
 575          return $keys;
 576      }
 577  
 578      /**
 579       * Gets an array of options to use as the serialiser.
 580       * @return array
 581       */
 582      public static function config_get_serialiser_options() {
 583          $options = array(
 584              Memcached::SERIALIZER_PHP => get_string('serialiser_php', 'cachestore_memcached')
 585          );
 586          if (Memcached::HAVE_JSON) {
 587              $options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached');
 588          }
 589          if (Memcached::HAVE_IGBINARY) {
 590              $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached');
 591          }
 592          return $options;
 593      }
 594  
 595      /**
 596       * Gets an array of hash options available during configuration.
 597       * @return array
 598       */
 599      public static function config_get_hash_options() {
 600          $options = array(
 601              Memcached::HASH_DEFAULT => get_string('hash_default', 'cachestore_memcached'),
 602              Memcached::HASH_MD5 => get_string('hash_md5', 'cachestore_memcached'),
 603              Memcached::HASH_CRC => get_string('hash_crc', 'cachestore_memcached'),
 604              Memcached::HASH_FNV1_64 => get_string('hash_fnv1_64', 'cachestore_memcached'),
 605              Memcached::HASH_FNV1A_64 => get_string('hash_fnv1a_64', 'cachestore_memcached'),
 606              Memcached::HASH_FNV1_32 => get_string('hash_fnv1_32', 'cachestore_memcached'),
 607              Memcached::HASH_FNV1A_32 => get_string('hash_fnv1a_32', 'cachestore_memcached'),
 608              Memcached::HASH_HSIEH => get_string('hash_hsieh', 'cachestore_memcached'),
 609              Memcached::HASH_MURMUR => get_string('hash_murmur', 'cachestore_memcached'),
 610          );
 611          return $options;
 612      }
 613  
 614      /**
 615       * Given the data from the add instance form this function creates a configuration array.
 616       *
 617       * @param stdClass $data
 618       * @return array
 619       */
 620      public static function config_get_configuration_array($data) {
 621          $lines = explode("\n", $data->servers);
 622          $servers = array();
 623          foreach ($lines as $line) {
 624              // Trim surrounding colons and default whitespace.
 625              $line = trim(trim($line), ":");
 626              // Skip blank lines.
 627              if ($line === '') {
 628                  continue;
 629              }
 630              $servers[] = explode(':', $line, 3);
 631          }
 632  
 633          $clustered = false;
 634          $setservers = array();
 635          if (isset($data->clustered)) {
 636              $clustered = true;
 637  
 638              $lines = explode("\n", $data->setservers);
 639              foreach ($lines as $line) {
 640                  // Trim surrounding colons and default whitespace.
 641                  $line = trim(trim($line), ":");
 642                  if ($line === '') {
 643                      continue;
 644                  }
 645                  $setserver = explode(':', $line, 3);
 646                  // We don't use weights, so display a debug message.
 647                  if (count($setserver) > 2) {
 648                      debugging('Memcached Set Server '.$setserver[0].' has too many parameters.');
 649                  }
 650                  $setservers[] = $setserver;
 651              }
 652          }
 653  
 654          $isshared = false;
 655          if (isset($data->isshared)) {
 656              $isshared = $data->isshared;
 657          }
 658  
 659          return array(
 660              'servers' => $servers,
 661              'compression' => $data->compression,
 662              'serialiser' => $data->serialiser,
 663              'prefix' => $data->prefix,
 664              'hash' => $data->hash,
 665              'bufferwrites' => $data->bufferwrites,
 666              'clustered' => $clustered,
 667              'setservers' => $setservers,
 668              'isshared' => $isshared
 669          );
 670      }
 671  
 672      /**
 673       * Allows the cache store to set its data against the edit form before it is shown to the user.
 674       *
 675       * @param moodleform $editform
 676       * @param array $config
 677       */
 678      public static function config_set_edit_form_data(moodleform $editform, array $config) {
 679          $data = array();
 680          if (!empty($config['servers'])) {
 681              $servers = array();
 682              foreach ($config['servers'] as $server) {
 683                  $servers[] = join(":", $server);
 684              }
 685              $data['servers'] = join("\n", $servers);
 686          }
 687          if (isset($config['compression'])) {
 688              $data['compression'] = (bool)$config['compression'];
 689          }
 690          if (!empty($config['serialiser'])) {
 691              $data['serialiser'] = $config['serialiser'];
 692          }
 693          if (!empty($config['prefix'])) {
 694              $data['prefix'] = $config['prefix'];
 695          }
 696          if (!empty($config['hash'])) {
 697              $data['hash'] = $config['hash'];
 698          }
 699          if (isset($config['bufferwrites'])) {
 700              $data['bufferwrites'] = (bool)$config['bufferwrites'];
 701          }
 702          if (isset($config['clustered'])) {
 703              $data['clustered'] = (bool)$config['clustered'];
 704          }
 705          if (!empty($config['setservers'])) {
 706              $servers = array();
 707              foreach ($config['setservers'] as $server) {
 708                  $servers[] = join(":", $server);
 709              }
 710              $data['setservers'] = join("\n", $servers);
 711          }
 712          if (isset($config['isshared'])) {
 713              $data['isshared'] = $config['isshared'];
 714          }
 715          $editform->set_data($data);
 716      }
 717  
 718      /**
 719       * Performs any necessary clean up when the store instance is being deleted.
 720       */
 721      public function instance_deleted() {
 722          if ($this->connection) {
 723              $connection = $this->connection;
 724          } else {
 725              $connection = new Memcached(crc32($this->name));
 726              $servers = $connection->getServerList();
 727              if (empty($servers)) {
 728                  foreach ($this->options as $key => $value) {
 729                      $connection->setOption($key, $value);
 730                  }
 731                  $connection->addServers($this->servers);
 732              }
 733          }
 734          // We have to flush here to be sure we are completely cleaned up.
 735          // Bad for performance but this is incredibly rare.
 736          @$connection->flush();
 737          unset($connection);
 738          unset($this->connection);
 739      }
 740  
 741      /**
 742       * Generates an instance of the cache store that can be used for testing.
 743       *
 744       * @param cache_definition $definition
 745       * @return cachestore_memcached|false
 746       */
 747      public static function initialise_test_instance(cache_definition $definition) {
 748  
 749          if (!self::are_requirements_met()) {
 750              return false;
 751          }
 752  
 753          $config = get_config('cachestore_memcached');
 754          if (empty($config->testservers)) {
 755              return false;
 756          }
 757  
 758          $configuration = array();
 759          $configuration['servers'] = explode("\n", $config->testservers);
 760          if (!empty($config->testcompression)) {
 761              $configuration['compression'] = $config->testcompression;
 762          }
 763          if (!empty($config->testserialiser)) {
 764              $configuration['serialiser'] = $config->testserialiser;
 765          }
 766          if (!empty($config->testprefix)) {
 767              $configuration['prefix'] = $config->testprefix;
 768          }
 769          if (!empty($config->testhash)) {
 770              $configuration['hash'] = $config->testhash;
 771          }
 772          if (!empty($config->testbufferwrites)) {
 773              $configuration['bufferwrites'] = $config->testbufferwrites;
 774          }
 775          if (!empty($config->testclustered)) {
 776              $configuration['clustered'] = $config->testclustered;
 777          }
 778          if (!empty($config->testsetservers)) {
 779              $configuration['setservers'] = explode("\n", $config->testsetservers);
 780          }
 781          if (!empty($config->testname)) {
 782              $name = $config->testname;
 783          } else {
 784              $name = 'Test memcached';
 785          }
 786  
 787          $store = new cachestore_memcached($name, $configuration);
 788          // If store is ready then only initialise.
 789          if ($store->is_ready()) {
 790              $store->initialise($definition);
 791          }
 792  
 793          return $store;
 794      }
 795  
 796      /**
 797       * Generates the appropriate configuration required for unit testing.
 798       *
 799       * @return array Array of unit test configuration data to be used by initialise().
 800       */
 801      public static function unit_test_configuration() {
 802          // If the configuration is not defined correctly, return only the configuration know about.
 803          if (!defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS')) {
 804              return [];
 805          }
 806          return ['servers' => explode("\n", TEST_CACHESTORE_MEMCACHED_TESTSERVERS)];
 807      }
 808  
 809      /**
 810       * Returns the name of this instance.
 811       * @return string
 812       */
 813      public function my_name() {
 814          return $this->name;
 815      }
 816  
 817      /**
 818       * Used to notify of configuration conflicts.
 819       *
 820       * The warnings returned here will be displayed on the cache configuration screen.
 821       *
 822       * @return string[] Returns an array of warnings (strings)
 823       */
 824      public function get_warnings() {
 825          global $CFG;
 826          $warnings = array();
 827          if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
 828              $bits = explode(':', $CFG->session_memcached_save_path, 3);
 829              $host = array_shift($bits);
 830              $port = (count($bits)) ? array_shift($bits) : '11211';
 831  
 832              foreach ($this->servers as $server) {
 833                  if ((string)$server[0] === $host && (string)$server[1] === $port) {
 834                      $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcached', $this->my_name());
 835                      break;
 836                  }
 837              }
 838          }
 839          return $warnings;
 840      }
 841  
 842      /**
 843       * Returns true if this cache store instance is both suitable for testing, and ready for testing.
 844       *
 845       * Cache stores that support being used as the default store for unit and acceptance testing should
 846       * override this function and return true if there requirements have been met.
 847       *
 848       * @return bool
 849       */
 850      public static function ready_to_be_used_for_testing() {
 851          return defined('TEST_CACHESTORE_MEMCACHED_TESTSERVERS');
 852      }
 853  }