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

4
require_once 'dmemcache.inc';
5

6 7 8 9 10
/**
 * Defines the period after which wildcard clears are not considered valid.
 */
define('MEMCACHE_WILDCARD_INVALIDATE', 86400 * 28);

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

13 14 15 16
class MemCacheDrupal implements DrupalCacheInterface {
  function __construct($bin) {
    $this->memcache = dmemcache_object($bin);
    $this->bin = $bin;
17 18 19 20 21 22

    $this->wildcard_timestamps = variable_get('memcache_wildcard_timestamps', array());
    $this->invalidate = variable_get('memcache_wildcard_invalidate', MEMCACHE_WILDCARD_INVALIDATE);
    $this->cache_lifetime = variable_get('cache_lifetime', 0);
    $this->cache_flush = variable_get('cache_flush_' . $this->bin);
    $this->flushed = min($this->cache_flush, REQUEST_TIME - $this->cache_lifetime);
23 24
  }
  function get($cid) {
25 26 27 28 29
    $cache = dmemcache_get($cid, $this->bin, $this->memcache);
    return $this->valid($cid, $cache) ? $cache : FALSE;
  }

  function getMultiple(&$cids) {
30
    $results = dmemcache_get_multi($cids, $this->bin, $this->memcache);
31
    foreach ($results as $cid => $result) {
32
      if (!$this->valid($result->cid, $result)) {
33
        // This object has expired, so don't return it.
34 35 36
        unset($results[$cid]);
      }
      else {
37 38
        // Remove items from the referenced $cids array that we are returning,
        // per the comment in cache_get_multiple() in includes/cache.inc.
39
        unset($cids[$result->cid]);
40 41 42 43 44 45
      }
    }
    return $results;
  }

  protected function valid($cid, $cache) {
46
    if (!isset($cache) || !is_object($cache)) {
47
      return FALSE;
Jeremy's avatar
Jeremy committed
48
    }
49

50 51 52
    // The wildcard_valid() function has overhead due to a call to
    // dmemcache_get_multi() to fetch possible wildcard flushes. Since some
    // bins never have wildcard clears with a cid, we can shortcut these checks.
53
    if (!empty($this->wildcard_timestamps[$this->bin]) &&
54
        max($this->wildcard_timestamps[$this->bin]) >= (REQUEST_TIME - $this->invalidate) &&
55
        !$this->wildcard_valid($cid, $cache))  {
56 57 58
      return FALSE;
    }

59
    // Determine when the current bin was last flushed.
60 61 62
    $item_flushed_globally = $cache->created && $this->cache_flush &&
                             $this->cache_lifetime &&
                             ($cache->created <= $this->flushed);
63

64 65
    // Look in the session to determine if this item was flushed for the
    // current user (ie, if they posted a comment and are viewing a cached page)
66
    $cache_bins = isset($_SESSION['cache_flush']) ? $_SESSION['cache_flush'] : NULL;
67 68 69
    $item_flushed_for_user = !empty($cache_bins) &&
                             isset($cache_bins[$this->bin]) &&
                             ($cache->created < $cache_bins[$this->bin]);
70 71
    if ($item_flushed_for_user) {
      return FALSE;
72
    }
73 74 75 76 77 78 79 80 81 82 83 84 85

    // 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.
86 87 88
    $item_expired = isset($cache->expire) &&
                    $cache->expire !== CACHE_PERMANENT &&
                    $cache->expire <= REQUEST_TIME;
89 90 91 92 93 94 95 96 97 98
    if ($item_flushed_globally || $item_expired) {
      // To avoid a stampede, return TRUE despite the item being expired if
      // a previous process set the stampede semaphore already. However only
      // do this if the data is less than 30 minutes stale.
      if ((REQUEST_TIME - $cache->expire) >= variable_get('memcache_max_staleness', 1800) ||
          dmemcache_add($cid . '_semaphore', '', variable_get('memcache_stampede_semaphore', 15), $this->bin)) {
        return FALSE;
      }
    }
    return TRUE;
99
  }
Steve Rude's avatar
Steve Rude committed
100

101
  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) {
102
    $created = REQUEST_TIME;
103

104 105 106 107 108 109
    // Create new cache object.
    $cache = new stdClass;
    $cache->cid = $cid;
    $cache->data = is_object($data) ? clone $data : $data;
    $cache->created = $created;
    $cache->headers = $headers;
110 111
    // Record the previous number of wildcard flushes affecting our cid.
    $cache->flushes = $this->wildcard_flushes($cid);
112 113 114
    if ($expire == CACHE_TEMPORARY) {
      // Convert CACHE_TEMPORARY (-1) into something that will live in memcache
      // until the next flush.
115
      $cache->expire = REQUEST_TIME + 2591999;
116 117 118 119 120
    }
    // 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().
121
      $cache->expire = REQUEST_TIME + $expire;
122 123 124 125
    }
    else {
      $cache->expire = $expire;
    }
126

127
    // We manually track the expire time in $cache->expire.  When the object
128 129 130
    // 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.
131
    dmemcache_set($cid, $cache, 0, $this->bin, $this->memcache);
132
  }
133 134

  function clear($cid = NULL, $wildcard = FALSE) {
135
    if (empty($cid) || $wildcard === TRUE) {
136 137 138
      if ($cid == '*') {
        $cid = '';
      }
139
      if ($this->cache_lifetime && empty($cid)) {
140 141 142 143
        // 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.
144
        variable_set("cache_flush_$this->bin", REQUEST_TIME);
145
        $this->flushed = REQUEST_TIME;
146 147 148 149 150

        // 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.
151
        if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) {
152 153 154 155 156
          $cache_bins = $_SESSION['cache_flush'];
        }
        else {
          $cache_bins = array();
        }
157
        $cache_bins[$this->bin] = REQUEST_TIME;
158 159 160
        $_SESSION['cache_flush'] = $cache_bins;
      }
      else {
161 162
        // Register a wildcard flush for current cid
        $this->wildcards($cid, TRUE);
163 164 165 166 167 168
      }
    }
    else {
      $cids = is_array($cid) ? $cid : array($cid);
      foreach ($cids as $cid) {
        dmemcache_delete($cid, $this->bin, $this->memcache);
169 170 171
      }
    }
  }
172

173
  /**
174 175 176
   * 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.
177 178 179 180 181 182
   */
  private function wildcard_flushes($cid) {
    return array_sum($this->wildcards($cid));
  }

  /**
183 184 185
   * 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.
186 187 188
   */
  private function wildcards($cid, $flush = FALSE) {
    static $wildcards = array();
189
    $matching = array();
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
    if (isset($this->wildcard_timestamps[$this->bin]) &&
      is_array($this->wildcard_timestamps[$this->bin])) {
      // Determine which lookups we need to perform to determine whether or not
      // our cid was impacted by a wildcard flush.
      $lookup = array();

      // Find statically cached wildcards, and determine possibly matching
      // wildcards for this cid based on a history of the lengths of past
      // valid wildcard flushes in this bin.
      foreach ($this->wildcard_timestamps[$this->bin] as $length => $timestamp) {
        if ($timestamp >= (REQUEST_TIME - $this->invalidate)) {
          $key = '.wildcard-' . substr($cid, 0, $length);
          $wildcard = dmemcache_key($key, $this->bin);
          if (isset($wildcards[$this->bin][$wildcard])) {
            $matching[$wildcard] = $wildcards[$this->bin][$wildcard];
          }
          else {
            $lookup[$wildcard] = $key;
          }
        }
      }

      // Do a multi-get to retrieve all possibly matching wildcard flushes.
      if (!empty($lookup)) {
        $values = dmemcache_get_multi($lookup, $this->bin, $this->memcache);
        if (is_array($values)) {
          // Prepare an array of matching wildcards.
          $matching = array_merge($matching, $values);
          // Store matches in the static cache.
          if (isset($wildcards[$this->bin])) {
            $wildcards[$this->bin] = array_merge($wildcards[$this->bin], $values);
          }
          else {
            $wildcards[$this->bin] = $values;
          }
          $lookup = array_diff_key($lookup, $values);
        }
228

229 230 231 232 233
        // Also store failed lookups in our static cache, so we don't have to
        // do repeat lookups on single page loads.
        foreach ($lookup as $wildcard => $key) {
          $wildcards[$this->bin][$wildcard] = 0;
        }
234 235 236
      }
    }
    if ($flush) {
237 238 239 240 241 242 243 244 245
      // Avoid too many calls to variable_set() by only recording a flush for
      // a fraction of the wildcard invalidation variable, per cid length.
      // Defaults to 28 / 4, or one week.
      $length = strlen($cid);
      if (!isset($this->wildcard_timestamps[$this->bin][$length]) ||
          ($_SERVER['REQUEST_TIME'] - $this->wildcard_timestamps[$this->bin][$length] > $this->invalidate / 4)) {
        $this->wildcard_timestamps = variable_get('memcache_wildcard_timestamps', array());
        $this->wildcard_timestamps[$this->bin][$length] = $_SERVER['REQUEST_TIME'];
        variable_set('memcache_wildcard_timestamps', $this->wildcard_timestamps);
246
      }
247 248 249 250
      $wildcard = dmemcache_key('.wildcard-' . $cid, $this->bin);
      if (isset($wildcards[$this->bin][$wildcard]) && $wildcards[$this->bin][$wildcard] != 0) {
        $this->memcache->increment($wildcard);
        $wildcards[$this->bin][$wildcard]++;
251 252
      }
      else {
253 254
        $wildcards[$this->bin][$wildcard] = 1;
        dmemcache_set('.wildcard-' . $cid, '1', 0, $this->bin);
255 256
      }
    }
257
    return $matching;
258 259 260 261 262 263
  }

  /**
   * Check if a wildcard flush has invalidated the current cached copy.
   */
  private function wildcard_valid($cid, $cache) {
264 265 266 267
    // Previously cached content won't have ->flushes defined.  We could
    // force flush, but instead leave this up to the site admin.
    $flushes = isset($cache->flushes) ? (int)$cache->flushes : 0;
    if ($flushes < (int)$this->wildcard_flushes($cid)) {
268 269 270 271 272
      return FALSE;
    }
    return TRUE;
  }

273 274 275
  function isEmpty() {
    // We do not know so err on the safe side?
    return FALSE;
276 277
  }
}
278