memcache.inc 8.26 KB
Newer Older
1
<?php
robertDouglass's avatar
robertDouglass committed
2
// $Id$
3

4
require_once 'dmemcache.inc';
5 6 7

/** Implementation of cache.inc with memcache logic included **/

8 9 10 11 12 13
class MemCacheDrupal implements DrupalCacheInterface {
  function __construct($bin) {
    $this->memcache = dmemcache_object($bin);
    $this->bin = $bin;
  }
  function get($cid) {
14 15 16 17 18
    $cache = dmemcache_get($cid, $this->bin, $this->memcache);
    return $this->valid($cid, $cache) ? $cache : FALSE;
  }

  function getMultiple(&$cids) {
19
    $results = dmemcache_get_multi($cids, $this->bin, $this->memcache);
20
    foreach ($results as $cid => $result) {
21
      if (!$this->valid($result->cid, $result)) {
22
        // This object has expired, so don't return it.
23 24 25
        unset($results[$cid]);
      }
      else {
26 27
        // Remove items from the referenced $cids array that we are returning,
        // per the comment in cache_get_multiple() in includes/cache.inc.
28
        unset($cids[$result->cid]);
29 30 31 32 33 34
      }
    }
    return $results;
  }

  protected function valid($cid, $cache) {
35 36
    if (!is_object($cache)) {
      return FALSE;
Jeremy's avatar
Jeremy committed
37
    }
38

39
    if (!$this->wildcard_valid($cid, $cache)) {
40 41 42
      return FALSE;
    }

43 44
    // Determine when the current bin was last flushed.
    $cache_flush = variable_get("cache_flush_$this->bin", 0);
Jeremy's avatar
Jeremy committed
45

46 47 48 49 50 51 52 53
    $cache_lifetime = variable_get('cache_lifetime', 0);
    $item_flushed_globally = $cache->created && $cache_flush && $cache_lifetime && ($cache->created < min($cache_flush, time() - $cache_lifetime));

    $cache_bins = isset($_SESSION['cache_flush']) ? $_SESSION['cache_flush'] : NULL;

    $item_flushed_for_user = is_array($cache_bins) && isset($cache_bins[$this->bin]) && ($cache->created < $cache_bins[$this->bin]);
    if ($item_flushed_for_user) {
      return FALSE;
54
    }
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

    // The item can be expired if:
    // - A liftetime is set and the item is older than both the lifetime and
    //   the global flush.
    // - The item has been create before the bin was flushed for this user.
    // - The item could simply expire.
    //
    // For the two global cases we try and grab a lock.  If we get the lock, we
    // return FALSE instead of the cached object which should cause it to be
    // rebuilt.  If we do not get the lock, we return the cached object despite
    // it's expired  The goal here is to avoid cache stampedes.
    // By default the cache stampede semaphore is held for 15 seconds.  This
    // can be adjusted by setting the memcache_stampede_semaphore variable.
    // TODO: Can we log when a sempahore expires versus being intentionally
    // freed to track when this is happening?
    $item_expired = isset($cache->expire) && $cache->expire !== CACHE_PERMANENT && $cache->expire <= time();
    return !(($item_flushed_globally || $item_expired) && dmemcache_add($cid .'_semaphore', '', variable_get('memcache_stampede_semaphore', 15), $this->bin));
72
  }
Steve Rude's avatar
Steve Rude committed
73

74 75
  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) {
    $created = time();
76

77 78 79 80 81 82
    // Create new cache object.
    $cache = new stdClass;
    $cache->cid = $cid;
    $cache->data = is_object($data) ? clone $data : $data;
    $cache->created = $created;
    $cache->headers = $headers;
83 84
    // Record the previous number of wildcard flushes affecting our cid.
    $cache->flushes = $this->wildcard_flushes($cid);
85 86 87 88 89 90 91 92 93 94 95 96 97 98
    if ($expire == CACHE_TEMPORARY) {
      // Convert CACHE_TEMPORARY (-1) into something that will live in memcache
      // until the next flush.
      $cache->expire = time() + 2591999;
    }
    // Expire time is in seconds if less than 30 days, otherwise is a timestamp.
    else if ($expire != CACHE_PERMANENT && $expire < 2592000) {
      // Expire is expressed in seconds, convert to the proper future timestamp
      // as expected in dmemcache_get().
      $cache->expire = time() + $expire;
    }
    else {
      $cache->expire = $expire;
    }
99

100 101 102 103 104
    // We manually track the expire time in $cache->expire.  When the object
    // expires, we only allow one request to rebuild it to avoid cache stampedes.
    // Other requests for the expired object while it is still being rebuilt get
    // the expired object.
    dmemcache_set($cid, $cache, 0, $this->bin, $this->memcache);
105
  }
106 107

  function clear($cid = NULL, $wildcard = FALSE) {
Jeremy's avatar
Jeremy committed
108

109
    if (empty($cid) || $wildcard === TRUE) {
110 111 112 113
      if ($cid == '*') {
        $cid = '';
      }
      if (variable_get('cache_lifetime', 0) && empty($cid)) {
114 115 116 117 118 119 120 121 122 123
        // Update the timestamp of the last global flushing of this bin.  When
        // retrieving data from this bin, we will compare the cache creation
        // time minus the cache_flush time to the cache_lifetime to determine
        // whether or not the cached item is still valid.
        variable_set("cache_flush_$this->bin", time());

        // We store the time in the current user's session which is saved into
        // the sessions table by sess_write().  We then simulate that the cache
        // was flushed for this user by not returning cached data to this user
        // that was cached before the timestamp.
124
        if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) {
125 126 127 128 129 130 131 132 133
          $cache_bins = $_SESSION['cache_flush'];
        }
        else {
          $cache_bins = array();
        }
        $cache_bins[$this->bin] = time();
        $_SESSION['cache_flush'] = $cache_bins;
      }
      else {
134 135
        // Register a wildcard flush for current cid
        $this->wildcards($cid, TRUE);
136 137 138 139 140 141
      }
    }
    else {
      $cids = is_array($cid) ? $cid : array($cid);
      foreach ($cids as $cid) {
        dmemcache_delete($cid, $this->bin, $this->memcache);
142 143 144
      }
    }
  }
145

146 147 148 149 150 151 152 153
  /**
   * We hash cids to keep them a consistent, managable length.  Alternative algorithms
   * can be specified if you're looking for better performance (benchmark first!).
   * Hash collissions are not a big deal, simply leads to all collided items being
   * flushed together.
   */
  function hash_cid($cid) {
    static $hashes = array();
154 155 156 157 158 159 160
    $memcache_hash = variable_get('memcache_hash', 'md5');
    if (function_exists($memcache_hash)) {
      $hashes[$cid] = $memcache_hash($cid);
    }
    else {
      $hashes[$cid] = $cid;
    }
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
    return $hashes[$cid];
  }

  /**
   * Determine all possible hashes that could match our cid.  We optimize away
   * the overhead of checking all possible matches by using multiget.
   */
  private function multihash_cid($cid) {
    static $hashes = array();
    if (!isset($hashes[$cid])) {
      for ($i = 0; $i <= strlen($cid); $i++) {
        $subcid = substr($cid, 0, $i);
        $hashes[$cid][$subcid] = '.wildcard-'. $this->bin . $this->hash_cid($subcid);
      }
    }
    return $hashes[$cid];
  }

  /**
   * Sum of all matching wildcards.  Checking any single cache item's flush value
   * against this single-value sum tells us whether or not a new wildcard flush
   * has affected the cached item.
   */
  private function wildcard_flushes($cid) {
    return array_sum($this->wildcards($cid));
  }

  /**
   * Utilize multiget to retrieve all possible wildcard matches, storing statically
   * so multiple cache requests for the same item on the same page load doesn't add
   * overhead.
   */
  private function wildcards($cid, $flush = FALSE) {
    static $wildcards = array();
    if (!isset($wildcard[$this->bin]) || !isset($wildcards[$this->bin][$cid])) {
      $multihash = $this->multihash_cid($cid);
      $wildcards[$this->bin][$cid] = dmemcache_get_multi($multihash, $this->bin);
      if (!is_array($wildcards[$this->bin][$cid])) {
        $wildcards[$this->bin][$cid] = array();
      }
    }
    if ($flush) {
      $hash = $this->hash_cid($cid);
      $wildcard = dmemcache_key('.wildcard-' . $this->bin . $hash, $this->bin);
      if (isset($wildcards[$this->bin][$cid][$wildcard])) {
        $mc = dmemcache_object($this->bin);
        $mc->increment($wildcard);
        $wildcards[$this->bin][$cid][$wildcard]++;
      }
      else {
        $wildcards[$this->bin][$cid][$wildcard] = 1;
        dmemcache_set('.wildcard-' . $this->bin . $hash, 1, 0, $this->bin);
      }
    }
    return $wildcards[$this->bin][$cid];
  }

  /**
   * Check if a wildcard flush has invalidated the current cached copy.
   */
  private function wildcard_valid($cid, $cache) {
    if ((int)$cache->flushes < (int)$this->wildcard_flushes($cid)) {
      return FALSE;
    }
    return TRUE;
  }

228 229 230
  function isEmpty() {
    // We do not know so err on the safe side?
    return FALSE;
231 232
  }
}
233