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 19 20 21
    $cache = dmemcache_get($cid, $this->bin, $this->memcache);
    return $this->valid($cid, $cache) ? $cache : FALSE;
  }

  function getMultiple(&$cids) {
    $results = dmemcache_get_multi(&$cids, $this->bin, $this->memcache);
    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 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
        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);
          if ($memcached_prefixes[$this->bin] === FALSE) {
            $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);
          while (!$mc->add($lock_key, 1, FALSE, 10)) {
            usleep(1000);
          }

          // Get a fresh copy of the prefix directory.
          $memcached_prefixes[$this->bin] = dmemcache_get('.prefixes', $this->bin);
          if ($memcached_prefixes[$this->bin] === FALSE) {
            $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