You are here

memcache_storage.api.inc in Memcache Storage 7

Provide class that processes memcached operations.

File

memcache_storage.api.inc
View source
<?php

/**
 * @file
 * Provide class that processes memcached operations.
 */

/**
 * Integrates with memcache API.
 */
class MemcacheStorageAPI {

  // List of available memcached connections.
  private static $connections;

  // Unique debug key.
  private static $debug_key = 0;

  /**
   * Connect to memcached clustes and returns memcached object on success.
   *
   * @param $bin
   *   Cache bin name. i.e. 'cache' or 'cache_bootstrap'.
   *
   * @return bool|object
   *   Memcache or memcached object is connection was successful. Otherwise FALSE.
   */
  public static function openConnection($bin = '') {

    // Get name of cluster for the selected cache bin storage.
    $cluster_name = self::getCacheBinCluster($bin);

    // Connect to the cluster and get memcached object.
    if (!isset(self::$connections[$cluster_name])) {

      // Store memcached object as a static object, to avoid duplicate connections to the same
      // memcached cluster but for different cache bins.
      self::$connections[$cluster_name] = self::connectToCluster($cluster_name);
    }
    return self::$connections[$cluster_name];
  }

  /**
   * Connect to memcached cluster and return memcached object.
   *
   * @param $cluster_name
   *   Name of memcached cluster.
   *
   * @return object
   *   Connected memcached object or FALSE if no connection.
   */
  protected static function connectToCluster($cluster_name) {

    // Load available servers and its cluster names from settings.php.
    $server_list = variable_get('memcache_servers', array(
      '127.0.0.1:11211' => 'default',
    ));

    // Search for all servers matching bin cluster.
    $cluster_servers = array();
    foreach ($server_list as $server => $name) {
      if ($name == $cluster_name) {
        $cluster_servers[] = $server;
      }
    }

    // No servers found for this cache bin.
    if (empty($cluster_servers)) {

      // Log error.
      self::logError('Could not find server(s) for cluster %cluster. Check your settings.php configuration.', array(
        '%cluster' => $cluster_name,
      ));

      // Return not initialized value.
      return FALSE;
    }
    $preferred = variable_get('memcache_extension');
    if (!empty($preferred) && class_exists($preferred)) {
      $extension = $preferred;
    }
    elseif (class_exists('Memcache')) {
      $extension = 'Memcache';
    }
    elseif (class_exists('Memcached')) {
      $extension = 'Memcached';
    }

    // Check if Memcache extension enabled.
    if (empty($extension)) {

      // Log error.
      self::logError('<a href="@url_memcache">Memcache</a> or <a href="@url_memcached">Memcached</a> extension is not available.', array(
        '@url_memcache' => 'http://pecl.php.net/package/memcache',
        '@url_memcached' => 'http://pecl.php.net/package/memcache',
      ));
      return FALSE;
    }

    // Create new memcached connection.
    $memcache = new $extension();

    // Collect all servers thas is unable to connect.
    $failed_connections = array();

    // Connect to every server.
    foreach ($cluster_servers as $server) {
      list($host, $port) = explode(':', $server);

      // Support unix sockets in the format 'unix:///path/to/socket'.
      if ($host == 'unix') {

        // PECL Memcache supports 'unix:///path/to/socket' path in ::addServer function,
        // while PECL Memcached use only '/path/to/socket' string for the same purpose.
        if ($extension == 'Memcache') {
          $host = $server;
        }
        elseif ($extension == 'Memcached') {
          $host = substr($server, 7);
        }

        // Port is always 0 for unix sockets.
        $port = 0;
      }

      // Adding new server for memcached connection.
      $persistent = variable_get('memcache_storage_persistent_connection', FALSE);
      $connected = @$memcache
        ->addServer($host, $port, $persistent);
      if (!$connected) {

        // Mark connection to this server as 'failed'.
        $failed_connections[] = $server;

        // Log error if unable to connect to server.
        self::logError('Could not connect to server %server (port %port).', array(
          '%server' => $host,
          '%port' => $port,
        ));
      }
    }

    // If memcached is unable to connect to all servers it means that
    // we have no connections at all, and could not start memcached connection.
    if (sizeof($failed_connections) == sizeof($cluster_servers)) {
      return FALSE;
    }

    // Provide compressThreshold support for PECL Memcached library.
    if ($extension == 'Memcache') {

      // For more information see http://www.php.net/manual/en/memcache.setcompressthreshold.php.
      $compress_threshold = variable_get('memcache_storage_compress_threshold', array(
        'threshold' => 20000,
        'min_savings' => 0.2,
      ));
      if (isset($compress_threshold['threshold']) && isset($compress_threshold['min_savings'])) {
        $memcache
          ->setCompressThreshold($compress_threshold['threshold'], $compress_threshold['min_savings']);
      }
    }
    elseif ($extension == 'Memcached') {
      $default_options = array(
        Memcached::OPT_COMPRESSION => FALSE,
        Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
      );

      // For more info about memcached constants see http://www.php.net/manual/en/memcached.constants.php.
      $memcached_options = variable_get('memcache_options', array());
      $memcached_options += $default_options;

      // Add SASL support.
      $sasl_auth = variable_get('memcache_sasl_auth', array());
      if (!empty($sasl_auth['user']) && !empty($sasl_auth['password'])) {

        // SASL auth works only with binary protocol.
        // Protocol has to be set before we call setSaslAuthData(), so we set
        // it here specially.
        $memcache
          ->setOption(Memcached::OPT_BINARY_PROTOCOL, TRUE);
        $memcache
          ->setSaslAuthData($sasl_auth['user'], $sasl_auth['password']);
      }

      // Set memcached options.
      // See http://www.php.net/manual/en/memcached.setoption.php.
      foreach ($memcached_options as $key => $value) {
        $memcache
          ->setOption($key, $value);
      }
    }
    return $memcache;
  }

  /**
   * Get single cache value from memcached pool.
   */
  public static function get($cache_id, $cache_bin = '') {
    $cache = self::getMultiple(array(
      $cache_id,
    ), $cache_bin);
    return isset($cache[$cache_id]) ? $cache[$cache_id] : FALSE;
  }

  /**
   * Get values from memcache storage.
   * Provide debug logging.
   *
   * @see http://www.php.net/manual/en/memcache.get.php
   */
  public static function getMultiple($cache_ids, $cache_bin = '') {

    // Get memcached object.
    $memcache = self::openConnection($cache_bin);

    // No memcached connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // Process cache keys.
    $cids = array();
    foreach ($cache_ids as $cache_id) {
      $safe_key = self::preprocessCacheKey($cache_id, $cache_bin);
      $cids[$safe_key] = $cache_id;
    }

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }

    // Fetch data from memcached pool.
    $cache = array();
    if ($memcache instanceof Memcache) {
      $cache = @$memcache
        ->get(array_keys($cids));
    }
    elseif ($memcache instanceof Memcached) {
      $cache = @$memcache
        ->getMulti(array_keys($cids));
    }

    // Return cache with correct keys.
    $output = array();
    if (!empty($cache)) {
      $cache_keys = array_keys($cache);
      foreach ($cache_keys as $cache_key) {
        $output[$cids[$cache_key]] = $cache[$cache_key];
      }
    }

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('get', $cache_bin, array_values($cids), $cids, $output);
    }
    return $output;
  }

  /**
   * Save data into memcached pool.
   * Provide debug logging.
   *
   * @see http://www.php.net/manual/en/memcache.set.php
   */
  public static function set($cache_id, $data, $expire, $cache_bin = '') {

    // Get memcache object.
    $memcache = self::openConnection($cache_bin);

    // No memcache connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // Build unique cache id.
    $cid = self::preprocessCacheKey($cache_id, $cache_bin);

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }

    // Set data to memcached pool.
    $result = FALSE;
    if ($memcache instanceof Memcache) {

      // Get compression mode.
      $compressed = variable_get('memcache_storage_compress_data') ? MEMCACHE_COMPRESSED : 0;
      $result = @$memcache
        ->set($cid, $data, $compressed, $expire);
    }
    elseif ($memcache instanceof Memcached) {
      $result = @$memcache
        ->set($cid, $data, $expire);
    }

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('set', $cache_bin, $cache_id, $cid, $result);
    }
    return $result;
  }

  /**
   * Save data into memcached pool if key doesn't exist.
   * Provide debug logging.
   *
   * @see http://www.php.net/manual/en/memcache.set.php
   */
  public static function add($cache_id, $data, $expire, $cache_bin = '') {

    // Get memcache object.
    $memcache = self::openConnection($cache_bin);

    // No memcache connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // Build unique cache id.
    $cid = self::preprocessCacheKey($cache_id, $cache_bin);

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }

    // Set data to memcached pool.
    $result = FALSE;
    if ($memcache instanceof Memcache) {

      // Get compression mode.
      $compressed = variable_get('memcache_storage_compress_data') ? MEMCACHE_COMPRESSED : 0;
      $result = @$memcache
        ->add($cid, $data, $compressed, $expire);
    }
    elseif ($memcache instanceof Memcached) {
      @$memcache
        ->add($cid, $data, $expire);
      $result = $memcache
        ->getResultCode() != Memcached::RES_SUCCESS ? FALSE : TRUE;
    }

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('add', $cache_bin, $cache_id, $cid, $result);
    }
    return $result;
  }

  /**
   * Replace existing data in memcache pool.
   * Provide debug logging.
   *
   * @see http://www.php.net/manual/en/memcache.replace.php
   */
  public static function replace($cache_id, $data, $expire, $cache_bin = '') {

    // Get memcache object.
    $memcache = self::openConnection($cache_bin);

    // No memcache connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // Build unique cache id.
    $cid = self::preprocessCacheKey($cache_id, $cache_bin);

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }

    // Replace data if exists.
    $result = FALSE;
    if ($memcache instanceof Memcache) {

      // Get compression mode.
      $compressed = variable_get('memcache_storage_compress_data') ? MEMCACHE_COMPRESSED : 0;
      $result = @$memcache
        ->replace($cid, $data, $compressed, $expire);
    }
    elseif ($memcache instanceof Memcached) {
      $result = @$memcache
        ->replace($cid, $data, $expire);
    }

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('replace', $cache_bin, $cache_id, $cid, $result);
    }
    return $result;
  }

  /**
   * Delete value from memcached pool.
   * Provide debug logging.
   *
   * @see http://www.php.net/manual/en/memcache.delete.php
   */
  public static function delete($cache_id, $cache_bin = '') {

    // Get memcache object.
    $memcache = self::openConnection($cache_bin);

    // No memcache connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // Build unique cache id.
    $cid = self::preprocessCacheKey($cache_id, $cache_bin);

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }

    // Delete cached data from memcached.
    $result = @$memcache
      ->delete($cid);

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('delete', $cache_bin, $cache_id, $cid, $result);
    }
    return $result;
  }

  /**
   * Delete multiple cache items from memcached pool.
   * Provide debug logging.
   */
  public static function deleteMultiple($cache_ids, $cache_bin = '') {

    // Get memcache object.
    $memcache = self::openConnection($cache_bin);

    // No memcache connection.
    if (empty($memcache)) {
      return FALSE;
    }

    // If current instance is Memcache which doesn't support deleteMulti
    // requests, then just execute delete commands one after another.
    if ($memcache instanceof Memcache) {
      foreach ($cache_ids as $cache_id) {
        self::delete($cache_id, $cache_bin);
      }
      return TRUE;
    }

    // Build unique cache ids.
    $cids = array();
    foreach ($cache_ids as $cache_id) {
      $safe_key = self::preprocessCacheKey($cache_id, $cache_bin);
      $cids[$safe_key] = $cache_id;
    }

    // Run initial debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::initialDebugActions();
    }
    $result = @$memcache
      ->deleteMulti(array_keys($cids));

    // Return cache with correct keys.
    $output = array();
    if (!empty($result)) {
      $cache_keys = array_keys($result);
      foreach ($cache_keys as $cache_key) {
        $output[$cids[$cache_key]] = $result[$cache_key];
      }
    }

    // Run final debug actions.
    if (variable_get('memcache_storage_debug', FALSE)) {
      self::finalDebugActions('delete', $cache_bin, array_values($cids), $cids, $output);
    }
    return $output;
  }

  /**
   * Immediately invalidates all existing items in a selected memcached cluster.
   *
   * @param $cluster_name
   *   Name of memcached cluster.
   *
   * @return bool
   *   Status of operation.
   */
  public static function flushCluster($cluster_name) {
    $memcache = self::connectToCluster($cluster_name);
    if (!$memcache) {
      return FALSE;
    }
    return @$memcache
      ->flush();
  }

  /**
   * Immediately invalidates all existing items in all memcached clusters.
   */
  public static function flushClusters() {
    $servers = variable_get('memcache_servers', array(
      '127.0.0.1:11211' => 'default',
    ));
    $clusters = array();
    foreach ($servers as $cluster_name) {
      if (!in_array($cluster_name, $clusters)) {
        $clusters[] = $cluster_name;
      }
    }
    foreach ($clusters as $cluster_name) {
      self::flushCluster($cluster_name);
    }
  }

  /**
   * Adds memcached key prefix and ensures that key is safe.
   *
   * @param $cache_id
   *   Cache ID.
   *
   * @param $cache_bin
   *   Name of cache bin.
   *
   * @return string
   *   Unique cache key.
   */
  protected static function preprocessCacheKey($cache_id, $cache_bin) {

    // Get key prefix from settings.php.
    $key_prefix = variable_get('memcache_storage_key_prefix', '');

    // Build unique cache key.
    $cache_key = $key_prefix ? $key_prefix . '-' : '';
    $cache_key .= $cache_bin ? $cache_bin . '-' : '';
    $cache_key .= $cache_id;

    // Add urlencode to avoid problems with different protocols.
    // If cache bin is 'cache_page' it means that we use external page cache.
    // Otherwise cache bin name is 'cache_page_[INDEX]'. We don't need to
    // encode url for external page cache because external servers may not be able
    // to urldecode this.
    $safe_key = $cache_bin == 'cache_page' ? $cache_key : urlencode($cache_key);

    // Memcache only supports key length up to 250 bytes. If we have generated
    // a longer key, hash it with md5 which will shrink the key down to 32 bytes
    // while still keeping it unique.
    if (strlen($safe_key) > 250) {
      $safe_key = md5($safe_key);
    }
    return $safe_key;
  }

  /**
   * Get name of cluster where cache bin should be stored.
   * Config loads from settings.php.
   *
   * @param $bin
   *   Cache bin name. i.e. 'cache' or 'cache_bootstrap'.
   *
   * @return string
   *   Cluster name.
   */
  protected static function getCacheBinCluster($bin) {

    // Static variable that stores cache bin
    // names and mapped clusters.
    static $bin_clusters = array();

    // If static cache doesn't contain info about
    // mapping then we should load it.
    if (empty($bin_clusters[$bin])) {

      // Example:
      // $conf['memcache_bins'] = array(
      //   'cache' => 'default',
      //   'cache_bootstrap' => 'default',
      //   'cache_page' => 'pages',
      // );
      $cluster_bins = variable_get('memcache_bins', array());

      // Remove suffix from cache bin name.
      $cache_bin = self::normalizeCacheBin($bin);

      // Get mapped cluster.
      $bin_clusters[$bin] = !empty($cluster_bins[$cache_bin]) ? $cluster_bins[$cache_bin] : 'default';
    }
    return $bin_clusters[$bin];
  }

  /**
   * Trigger error if something goes wrong.
   *
   * @param $message
   * @param array $variables
   */
  protected static function logError($message, $variables = array()) {

    // We can't use watchdog because this happens in a bootstrap phase
    // where watchdog is non-functional. Register a shutdown handler
    // instead so it gets recorded at the end of page load.
    register_shutdown_function('watchdog', 'memcache_storage', $message, $variables, WATCHDOG_ERROR);
  }

  /**
   * First step of debug logging.
   * Start timer and get usage of memory before memcache action.
   */
  protected static function initialDebugActions() {
    $mem_usage =& drupal_static('memcache_storage_debug_mem_usage', array());

    // Start count time spent on setting data to memcache.
    timer_start(self::$debug_key);

    // Get memory usage before set new value into cache.
    $mem_usage[self::$debug_key] = memory_get_usage();
  }

  /**
   * Last step of debug logging.
   *
   * @param $method
   *   Memcache action name. For example, 'set'.
   *
   * @param $cache_bin
   *   Cache bin name where action performs.
   *
   * @param $cache_id
   *   Unprocessed Cache ID.
   *
   * @param $memcache_id
   *   Processed Cache ID right before memcache action.
   *
   * @param $result
   *   Results of action execution.
   */
  protected static function finalDebugActions($method, $cache_bin, $cache_id, $memcache_id, $result) {

    // Stop count timer.
    timer_stop(self::$debug_key);

    // Calculate memory usage.
    $mem_usage =& drupal_static('memcache_storage_debug_mem_usage', array());
    $used_memory = memory_get_usage() - $mem_usage[self::$debug_key];

    // Processes cache bin name (remove suffix _[INDEX] if exists).
    $cache_bin_original = self::normalizeCacheBin($cache_bin);

    // Get array with debug data.
    $memcache_storage_debug_output =& drupal_static('memcache_storage_debug_output');
    if (is_string($cache_id)) {
      $cache_id = array(
        $cache_id,
      );
    }

    // Log entries about memcache action.
    foreach ($cache_id as $cid) {
      $unique_class = str_replace(array(
        ' ',
        '_',
        '/',
        '[',
        ']',
        ':',
      ), '-', $cache_bin . '-' . $cid);
      $clear_link = array(
        '#theme' => 'link',
        '#text' => 'Delete',
        '#path' => 'memcache_storage/delete/nojs/' . $cache_bin . '/' . $cid,
        '#options' => array(
          'attributes' => array(
            'class' => array(
              'use-ajax',
              $unique_class,
            ),
          ),
          'html' => FALSE,
        ),
      );

      // Get result string (HIT or MISS).
      if (is_array($result)) {
        $result_status = isset($result[$cid]) && $result[$cid] !== Memcached::RES_NOTFOUND;
        $result_string = !empty($result_status) ? 'HIT' : 'MISS';
      }
      else {
        $result_string = $result ? 'HIT' : 'MISS';
      }
      $memcache_storage_debug_output[] = array(
        'action' => count($cache_id) > 1 ? $method . 'Multiple' : $method,
        'timer' => timer_read(self::$debug_key),
        'memory' => $used_memory,
        'result' => $result_string,
        'bin' => $cache_bin_original,
        'cid' => $cid,
        'mem_key' => is_array($memcache_id) ? array_search($cid, $memcache_id) : $memcache_id,
        'clear_link' => $cid != 'memcache_storage_bin_indexes' ? $clear_link : '',
        'token' => $cache_bin . '-' . $cid,
      );
    }

    // Increase timer to get new time results.
    self::$debug_key++;
  }

  /**
   * Removes suffix from cache bin name.
   *
   * @param $cache_bin
   *   Name of cache bin.
   *
   * @return string
   *   Return cache bin name without suffix.
   */
  private static function normalizeCacheBin($cache_bin) {
    $cache_bin_suffix = substr($cache_bin, strrpos($cache_bin, '_') + 1);
    if (ctype_digit($cache_bin_suffix)) {
      $cache_bin = substr($cache_bin, 0, strrpos($cache_bin, '_'));
    }
    return $cache_bin;
  }

}

Classes

Namesort descending Description
MemcacheStorageAPI Integrates with memcache API.