dmemcache.inc 13.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
<?php

/*
 * Core dmemcache functions required by:
 *   memcache.inc
 *   memcache.db.inc
 *   session-memcache.inc
 *   session-memcache.db.inc
 */

global $_memcache_statistics;
12
$_memcache_statistics = array();
13 14 15 16 17 18 19 20

/*
 * A memcache API for Drupal.
 */

/**
 *  Place an item into memcache
 *
21
 * @param $key The string with which you will retrieve this item later.
22
 * @param $value The item to be stored.
23 24 25 26 27 28 29 30 31
 * @param $exp Parameter expire is expiration time in seconds. If it's 0, the
 *   item never expires (but memcached server doesn't guarantee this item to be
 *   stored all the time, it could be deleted from the cache to make place for
 *   other items).
 * @param $bin The name of the Drupal subsystem that is making this call.
 *   Examples could be 'cache', 'alias', 'taxonomy term' etc. It is possible to
 *   map different $bin values to different memcache servers.
 * @param $mc Optionally pass in the memcache object.  Normally this value is
 *   determined automatically based on the bin the object is being stored to.
32 33 34
 *
 * @return bool
 */
35
function dmemcache_set($key, $value, $exp = 0, $bin = 'cache', $mc = NULL) {
36
  global $_memcache_statistics;
37 38
  $full_key = dmemcache_key($key, $bin);
  $_memcache_statistics[] = array('set', $bin, $full_key, '');
39
  if ($mc || ($mc = dmemcache_object($bin))) {
40
    if ($mc instanceof Memcached) {
41
      return $mc->set($full_key, $value, $exp);
42 43
    }
    else {
44
      return $mc->set($full_key, $value, MEMCACHE_COMPRESSED, $exp);
45 46 47 48 49
    }
  }
  return FALSE;
}

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
/**
 *  Add an item into memcache
 *
 * @param $key The string with which you will retrieve this item later.
 * @param $value The item to be stored.
 * @param $exp Parameter expire is expiration time in seconds. If it's 0, the
 *   item never expires (but memcached server doesn't guarantee this item to be
 *   stored all the time, it could be deleted from the cache to make place for
 *   other items).
 * @param $bin The name of the Drupal subsystem that is making this call.
 *   Examples could be 'cache', 'alias', 'taxonomy term' etc. It is possible
 *   to map different $bin values to different memcache servers.
 * @param $mc Optionally pass in the memcache object.  Normally this value is
 *   determined automatically based on the bin the object is being stored to.
 * @param $flag If using the older memcache PECL extension as opposed to the
 *   newer memcached PECL extension, the MEMCACHE_COMPRESSED flag can be set
 *   to use zlib to store a compressed copy of the item.  This flag option is
 *   completely ignored when using the newer memcached PECL extension.
 *
 * @return bool
 */
71 72
function dmemcache_add($key, $value, $exp = 0, $bin = 'cache', $mc = NULL, $flag = FALSE) {
  global $_memcache_statistics;
73 74
  $full_key = dmemcache_key($key, $bin);
  $_memcache_statistics[] = array('add', $bin, $full_key, '');
75
  if ($mc || ($mc = dmemcache_object($bin))) {
76
    if ($mc instanceof Memcached) {
Jeremy's avatar
Jeremy committed
77
      return $mc->add($full_key, $value, $exp);
78 79
    }
    else {
Jeremy's avatar
Jeremy committed
80
      return $mc->add($full_key, $value, $flag, $exp);
81 82
    }
  }
83
  return FALSE;
84 85
}

86 87 88 89 90 91 92 93
/**
 * Retrieve a value from the cache.
 *
 * @param $key The key with which the item was stored.
 * @param $bin The bin in which the item was stored.
 *
 * @return The item which was originally saved or FALSE
 */
94
function dmemcache_get($key, $bin = 'cache', $mc = NULL) {
95
  global $_memcache_statistics;
96 97 98
  $full_key = dmemcache_key($key, $bin);
  $statistics = array('get', $bin, $full_key);
  $success = '0';
99 100 101 102 103 104 105 106 107 108 109
  if ($mc || $mc = dmemcache_object($bin)) {
    $track_errors = ini_set('track_errors', '1');
    $php_errormsg = '';

    $result = @$mc->get($full_key);
    $statistics[] = (bool) $result;
    $_memcache_statistics[] = $statistics;

    if (!empty($php_errormsg)) {
      register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
      $php_errormsg = '';
110
    }
111
    ini_set('track_errors', $track_errors);
112
  }
113

114
  return $result;
115 116 117 118 119 120 121 122 123 124 125 126
}

/**
 * Retrieve multiple values from the cache.
 *
 * @param $keys The keys with which the items were stored.
 * @param $bin The bin in which the item was stored.
 *
 * @return The item which was originally saved or FALSE
 */
function dmemcache_get_multi($keys, $bin = 'cache', $mc = NULL) {
  global $_memcache_statistics;
127 128 129 130 131 132 133
  $full_keys = array();
  $statistics = array();
  foreach ($keys as $key => $cid) {
    $full_key = dmemcache_key($cid, $bin);
    $statistics[$full_key] = array('getMulti', $bin, $full_key);
    $full_keys[] = $full_key;
  }
134 135
  $results = array();
  if ($mc || ($mc = dmemcache_object($bin))) {
136
    if ($mc instanceof Memcached) {
137 138
      $results = $mc->getMulti($full_keys);
    }
139
    elseif ($mc instanceof Memcache) {
140 141 142 143 144 145 146 147 148 149
      $track_errors = ini_set('track_errors', '1');
      $php_errormsg = '';

      $results = @$mc->get($full_keys);

      if (!empty($php_errormsg)) {
        register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_get_multi: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
        $php_errormsg = '';
      }
      ini_set('track_errors', $track_errors);
150 151
    }
  }
152 153 154 155
  foreach ($statistics as $key => $values) {
    $values[] = isset($results[$key]) ? '1': '0';
    $_memcache_statistics[] = $values;
  }
156 157 158 159 160

  // If $results is FALSE, convert it to an empty array.
  if (!$results) {
    $results = array();
  }
161 162 163 164 165 166 167 168 169

  // Convert the full keys back to the cid.
  $cid_results = array();
  foreach ($results as $value) {
    if (is_object($value)) {
      $cid_results[$value->cid] = $value;
    }
  }
  return $cid_results;
170 171 172 173 174 175 176 177 178 179
}

/**
 * Deletes an item from the cache.
 *
 * @param $key The key with which the item was stored.
 * @param $bin The bin in which the item was stored.
 *
 * @return Returns TRUE on success or FALSE on failure.
 */
180
function dmemcache_delete($key, $bin = 'cache', $mc = NULL) {
181 182 183
  global $_memcache_statistics;
  $full_key = dmemcache_key($key, $bin);
  $_memcache_statistics[] = array('delete', $bin, $full_key, '');
184
  if ($mc || ($mc = dmemcache_object($bin))) {
185
    return $mc->delete($full_key, 0);
186 187 188 189 190 191 192 193 194 195 196 197 198 199
  }
  return FALSE;
}

/**
 * Immediately invalidates all existing items. dmemcache_flush doesn't actually free any
 * resources, it only marks all the items as expired, so occupied memory will be overwritten by
 * new items.
 *
 * @param $bin The bin to flush. Note that this will flush all bins mapped to the same server
 *   as $bin. There is no way at this time to empty just one bin.
 *
 * @return Returns TRUE on success or FALSE on failure.
 */
200
function dmemcache_flush($bin = 'cache', $mc = NULL) {
201 202
  global $_memcache_statistics;
  $_memcache_statistics[] = array('flush', $bin, '', '');
203
  if ($mc || ($mc = dmemcache_object($bin))) {
204
    return memcache_flush($mc);
205 206 207 208
  }
}

function dmemcache_stats($bin = 'cache', $type = '') {
209 210 211 212 213 214 215 216
  // resolve requests for 'default' type to ''
  if ($type == 'default') {
    $type = '';
  }
  // resolve requests for 'default' bin to 'cache'.
  if ($bin == 'default') {
    $bin = 'cache';
  }
217
  if ($mc = dmemcache_object($bin)) {
218
    if ($mc instanceof Memcached) {
219 220
      return $mc->getStats();
    }
221 222
    // The PHP Memcache extension 3.x version throws an error if the stats
    // type is NULL or not in {reset, malloc, slabs, cachedump, items, sizes}.
223
    // If $type is 'default', then no parameter should be passed to the
224 225
    // Memcache memcache_get_extended_stats() function.
    if ($type == 'default' || $type == '') {
226
      return $mc->getExtendedStats();
227 228
    }
    else {
229
      return $mc->getExtendedStats($type);
230
    }
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
  }
}

/**
 * Returns an Memcache object based on the bin requested. Note that there is
 * nothing preventing developers from calling this function directly to get the
 * Memcache object. Do this if you need functionality not provided by this API
 * or if you need to use legacy code. Otherwise, use the dmemcache (get, set,
 * delete, flush) API functions provided here.
 *
 * @param $bin The bin which is to be used.
 *
 * @param $flush Rebuild the bin/server/cache mapping.
 *
 * @return an Memcache object or FALSE.
 */
function dmemcache_object($bin = NULL, $flush = FALSE) {
248
  static $extension, $memcacheCache = array(), $memcache_servers, $memcache_bins, $memcache_persistent;
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

  if (!isset($extension)) {
    // If an extension is specified in settings.php, use that when available.
    $preferred = variable_get('memcache_extension', NULL);
    if (isset($preferred) && class_exists($preferred)) {
      $extension = $preferred;
    }
    // If no extension is set, default to Memcache.
    // The Memcached extension has some features that the older extension lacks
    // but also an unfixed bug that affects cache clears.
    // @see http://pecl.php.net/bugs/bug.php?id=16829
    elseif (class_exists('Memcache')) {
      $extension = 'Memcache';
    }
    elseif (class_exists('Memcached')) {
      $extension = 'Memcached';
    }
266 267 268 269 270 271 272 273

    // Indicate whether to connect to memcache using a persistent connection.
    // Note: this only affects the Memcache PECL extension, and does not
    // affect the Memcached PECL extension.  For a detailed explanation see:
    //  http://drupal.org/node/822316#comment-4427676
    if (!isset($memcache_persistent)) {
      $memcache_persistent = variable_get('memcache_persistent', FALSE);
    }
274
  }
275 276 277

  if ($flush) {
    foreach ($memcacheCache as $cluster) {
278
      memcache_close($cluster);
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    }
    $memcacheCache = array();
  }

  if (empty($memcacheCache) || empty($memcacheCache[$bin])) {
    // $memcache_servers and $memcache_bins originate from settings.php.
    // $memcache_servers_custom and $memcache_bins_custom get set by
    // memcache.module. They are then merged into $memcache_servers and
    // $memcache_bins, which are statically cached for performance.
    if (empty($memcache_servers)) {
      // Values from settings.php
      $memcache_servers = variable_get('memcache_servers', array('127.0.0.1:11211' => 'default'));
      $memcache_bins    = variable_get('memcache_bins', array('cache' => 'default'));
    }

    // If there is no cluster for this bin in $memcache_bins, cluster is 'default'.
    $cluster = empty($memcache_bins[$bin]) ? 'default' : $memcache_bins[$bin];

    // If this bin isn't in our $memcache_bins configuration array, and the
    // 'default' cluster is already initialized, map the bin to 'cache' because
    // we always map the 'cache' bin to the 'default' cluster.
    if (empty($memcache_bins[$bin]) && !empty($memcacheCache['cache'])) {
      $memcacheCache[$bin] = &$memcacheCache['cache'];
    }
    else {
      // Create a new Memcache object. Each cluster gets its own Memcache object.
305
      if ($extension == 'Memcached') {
306
        $memcache = new Memcached;
307 308
        $default_opts = array(
          Memcached::OPT_COMPRESSION => FALSE,
309 310
          Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
        );
311 312 313 314
        foreach ($default_opts as $key => $value) {
          $memcache->setOption($key, $value);
        }
        $memconf = variable_get('memcache_options', array());
315 316 317
        foreach ($memconf as $key => $value) {
          $memcache->setOption($key, $value);
        }
318
      }
319
      elseif ($extension == 'Memcache') {
320 321 322 323 324 325
        $memcache = new Memcache;
      }
      else {
        drupal_set_message(t('You must enable the PECL memcached or memcache extension to use memcache.inc.'), 'error');
        return;
      }
326 327 328 329 330 331 332 333
      // A variable to track whether we've connected to the first server.
      $init = FALSE;

      // Link all the servers to this cluster.
      foreach ($memcache_servers as $s => $c) {
        if ($c == $cluster) {
          list($host, $port) = explode(':', $s);

334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
          // Using the Memcache PECL extension.
          if ($memcache instanceof Memcache) {
            // When using the PECL memcache extension, we must use ->(p)connect
            // for the first connection.
            if (!$init) {
              $track_errors = ini_set('track_errors', '1');
              $php_errormsg = '';

              if ($memcache_persistent && @$memcache->pconnect($host, $port)) {
                $init = TRUE;
              }
              elseif (!$memcache_persistent && @$memcache->connect($host, $port)) {
                $init = TRUE;
              }

              if (!empty($php_errormsg)) {
                register_shutdown_function('watchdog', 'memcache', 'Exception caught in dmemcache_object: !msg', array('!msg' => $php_errormsg), WATCHDOG_WARNING);
                $php_errormsg = '';
              }
              ini_set('track_errors', $track_errors);
            }
            else {
              $memcache->addServer($host, $port, $memcache_persistent);
357 358 359
            }
          }
          else {
360 361 362
            if ($memcache->addServer($host, $port) && !$init) {
              $init = TRUE;
            }
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
          }
        }
      }

      if ($init) {
        // Map the current bin with the new Memcache object.
        $memcacheCache[$bin] = $memcache;

        // Now that all the servers have been mapped to this cluster, look for
        // other bins that belong to the cluster and map them too.
        foreach ($memcache_bins as $b => $c) {
          if ($c == $cluster && $b != $bin) {
            // Map this bin and cluster by reference.
            $memcacheCache[$b] = &$memcacheCache[$bin];
          }
        }
      }
    }
  }

  return empty($memcacheCache[$bin]) ? FALSE : $memcacheCache[$bin];
}

function dmemcache_key($key, $bin = 'cache') {
387 388 389 390 391 392 393 394
  $prefix = '';
  if ($prefix = variable_get('memcache_key_prefix', '')) {
    $prefix .= '-';
  }
  // When simpletest is running, emulate the simpletest database prefix here
  // to avoid the child site setting cache entries in the parent site.
  if (isset($GLOBALS['drupal_test_info']['test_run_id'])) {
    $prefix .= $GLOBALS['drupal_test_info']['test_run_id'];
395 396 397 398 399 400 401 402
  }
  $full_key = urlencode($prefix . $bin . '-' . $key);

  // Memcache only supports key lengths up to 250 bytes.  If we have generated
  // a longer key, hash it with sha1 which will shrink the key down to 40 bytes
  // while still keeping it unique.
  if (strlen($full_key) > 250) {
    $full_key = $prefix . $bin . '-' . sha1($key);
403
  }
404

405
  return $full_key;
406
}