memcache.inc 9.3 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 21
    foreach ($results as $cid => $result) {
      if (!$this->valid($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 29 30 31 32 33 34
        unset($cids[$cid]);
      }
    }
    return $results;
  }

  protected function valid($cid, $cache) {
Jeremy's avatar
Jeremy committed
35 36 37 38 39 40 41
    global $memcached_prefixes, $memcached_counters;
    if (!isset($memcached_prefixes)) {
      $memcached_prefixes = array();
    }
    if (!isset($memcached_counters)) {
      $memcached_counters = array();
    }
42 43 44 45 46

    if (!is_object($cache)) {
      return FALSE;
    }

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

50 51 52 53 54
    // Load the prefix directory.
    if (!isset($memcached_prefixes[$this->bin])) {
      $memcached_prefixes[$this->bin] = dmemcache_get('.prefixes', $this->bin);
      if (!is_array($memcached_prefixes[$this->bin])) {
        $memcached_prefixes[$this->bin] = array();
55
      }
56 57 58 59 60 61 62 63 64 65 66 67 68 69
      $memcached_counters[$this->bin] = array();
    }
    // Check if the item being fetched matches any prefixes.
    foreach ($memcached_prefixes[$this->bin] as $prefix) {
      if (substr($cid, 0, strlen($prefix)) == $prefix) {
        // On a match, check if we already know the current counter value.
        if (!isset($memcached_counters[$this->bin][$prefix])) {
          $memcached_counters[$this->bin][$prefix] = dmemcache_get('.prefix.' . $prefix, $this->bin);
        }
        // If a matching prefix for this item was cleared after storing it,
        // it is invalid.
        if (!isset($cache->counters[$prefix]) || $cache->counters[$prefix] < $memcached_counters[$this->bin][$prefix]) {
          return FALSE;
        }
70 71
      }
    }
72

73 74 75 76 77 78 79 80
    $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;
81
    }
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98

    // 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));
99
  }
Steve Rude's avatar
Steve Rude committed
100

101
  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) {
Jeremy's avatar
Jeremy committed
102
    global $memcached_prefixes, $memcached_counters;
103
    $created = time();
104

105
    if (!isset($memcached_prefixes[$this->bin]) || !is_array($memcached_prefixes[$this->bin])) {
Jeremy's avatar
Jeremy committed
106
      $memcached_prefixes[$this->bin] = dmemcache_get('.prefixes', $this->bin);
107
      if (!is_array($memcached_prefixes[$this->bin])) {
Jeremy's avatar
Jeremy committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
        $memcached_prefixes[$this->bin] = array();
      }
      $memcached_counters[$this->bin] = array();
    }

    $counters = array();
    // Check if the item being stored matches any prefixes.
    foreach ($memcached_prefixes[$this->bin] as $prefix) {
      if (substr($cid, 0, strlen($prefix)) == $prefix) {
        // On a match, check if we already know the current counter value.
        if (!isset($memcached_counters[$this->bin][$prefix])) {
          $memcached_counters[$this->bin][$prefix] = dmemcache_get('.prefix.' . $prefix, $this->bin);
        }
        $counters[$prefix] = $memcached_counters[$this->bin][$prefix];
      }
    }

125 126 127 128 129 130
    // Create new cache object.
    $cache = new stdClass;
    $cache->cid = $cid;
    $cache->data = is_object($data) ? clone $data : $data;
    $cache->created = $created;
    $cache->headers = $headers;
Jeremy's avatar
Jeremy committed
131
    $cache->counters = $counters;
132 133 134 135 136 137 138 139 140 141 142 143 144 145
    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;
    }
146

147 148 149 150 151
    // 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);
152
  }
153 154

  function clear($cid = NULL, $wildcard = FALSE) {
Jeremy's avatar
Jeremy committed
155 156
    global $memcached_prefixes, $memcached_counters;

157
    if (empty($cid) || $wildcard === TRUE) {
158 159 160 161 162 163 164 165 166 167 168
      if (variable_get('cache_lifetime', 0)) {
        // 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.
169
        if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) {
170 171 172 173 174 175 176 177 178
          $cache_bins = $_SESSION['cache_flush'];
        }
        else {
          $cache_bins = array();
        }
        $cache_bins[$this->bin] = time();
        $_SESSION['cache_flush'] = $cache_bins;
      }
      else {
Jeremy's avatar
Jeremy committed
179 180 181 182 183 184 185 186 187 188
        if ($cid == '*') {
          $cid = '';
        }

        // Get a memcached object for complex operations.
        $mc = dmemcache_object($this->bin);

        // Load the prefix directory.
        if (!isset($memcached_prefixes[$this->bin])) {
          $memcached_prefixes[$this->bin] = dmemcache_get('.prefixes', $this->bin);
Jeremy's avatar
Jeremy committed
189
          if (!is_array($memcached_prefixes[$this->bin])) {
Jeremy's avatar
Jeremy committed
190 191 192 193 194 195 196 197 198
            $memcached_prefixes[$this->bin] = array();
          }
        }

        // Ensure the prefix being cleared is listed, if not, atomically add it.
        // Adding new prefixes should be rare.
        if (!in_array($cid, $memcached_prefixes[$this->bin])) {
          // Acquire a semaphore.
          $lock_key = dmemcache_key('.prefixes.lock', $this->bin);
Jeremy's avatar
Jeremy committed
199
          while (!dmemcache_add($lock_key, 1, 10)) {
Jeremy's avatar
Jeremy committed
200 201 202 203 204
            usleep(1000);
          }

          // Get a fresh copy of the prefix directory.
          $memcached_prefixes[$this->bin] = dmemcache_get('.prefixes', $this->bin);
Jeremy's avatar
Jeremy committed
205
          if (!is_array($memcached_prefixes[$this->bin])) {
Jeremy's avatar
Jeremy committed
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
            $memcached_prefixes[$this->bin] = array();
          }

          // Only add the prefix if it's not in the updated directory.
          if (!in_array($cid, $memcached_prefixes[$this->bin])) {
            // Add the new prefix.
            $memcached_prefixes[$this->bin][] = $cid;

            // Store to memcached.
            dmemcache_set('.prefixes', $memcached_prefixes[$this->bin], 0, $this->bin);

            // Set the clearing counter to zero.
            dmemcache_set('.prefix.' . $cid, 0, 0, $this->bin);
          }

          // Release the semaphore.
          dmemcache_delete('.prefixes.lock', $this->bin);
        }

        // Increment the prefix clearing counter.
        $counter_key = dmemcache_key('.prefix.' . $cid, $this->bin);
        $memcached_counters[$this->bin][$cid] = $mc->increment($counter_key);
228 229 230 231 232 233
      }
    }
    else {
      $cids = is_array($cid) ? $cid : array($cid);
      foreach ($cids as $cid) {
        dmemcache_delete($cid, $this->bin, $this->memcache);
234 235 236
      }
    }
  }
237

238 239 240
  function isEmpty() {
    // We do not know so err on the safe side?
    return FALSE;
241 242
  }
}
243