memcache.inc 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. <?php
  2. /**
  3. * @file
  4. * Implementation of cache.inc with memcache logic included.
  5. */
  6. require_once dirname(__FILE__) . '/dmemcache.inc';
  7. /**
  8. * Defines the period after which wildcard clears are not considered valid.
  9. */
  10. define('MEMCACHE_WILDCARD_INVALIDATE', 86400 * 28);
  11. define('MEMCACHE_CONTENT_CLEAR', 'MEMCACHE_CONTENT_CLEAR');
  12. /**
  13. * Implementation of cache.inc with memcache logic included
  14. */
  15. class MemCacheDrupal implements DrupalCacheInterface {
  16. protected $memcache;
  17. /**
  18. * Constructs a MemCacheDrupal object.
  19. *
  20. * @param string $bin
  21. * The cache bin for which the object is created.
  22. */
  23. public function __construct($bin) {
  24. $this->memcache = dmemcache_object($bin);
  25. $this->bin = $bin;
  26. // If page_cache_without_database is enabled, we have to manually load the
  27. // $conf array out of cache_bootstrap.
  28. static $variables_loaded = FALSE;
  29. // NOTE: We don't call drupal_get_bootstrap_phase() because this would
  30. // break all 7.x Drupal installations prior to 7.33. For more information
  31. // see https://www.drupal.org/node/667098.
  32. if (drupal_bootstrap(NULL, FALSE) < DRUPAL_BOOTSTRAP_VARIABLES && !$variables_loaded) {
  33. global $conf;
  34. $variables_loaded = TRUE;
  35. // Try loading variables from cache. If that fails, we have to bootstrap
  36. // further in order to fetch them.
  37. if ($cached = cache_get('variables', 'cache_bootstrap')) {
  38. $variables = $cached->data;
  39. // Make sure variable overrides are applied, see variable_initialize().
  40. foreach ($conf as $name => $value) {
  41. $variables[$name] = $value;
  42. }
  43. $conf = $variables;
  44. }
  45. else {
  46. drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
  47. }
  48. }
  49. $this->reloadVariables();
  50. }
  51. /**
  52. * Implements DrupalCacheInterface::get().
  53. */
  54. public function get($cid) {
  55. // Clean up.
  56. $this->garbageCollection();
  57. $cache = dmemcache_get($cid, $this->bin, $this->memcache);
  58. return $this->valid($cid, $cache) ? $cache : FALSE;
  59. }
  60. /**
  61. * Implements DrupalCacheInterface::getMultiple().
  62. */
  63. public function getMultiple(&$cids) {
  64. // Clean up.
  65. $this->garbageCollection();
  66. $results = dmemcache_get_multi($cids, $this->bin, $this->memcache);
  67. foreach ($results as $cid => $result) {
  68. if (!$this->valid($cid, $result)) {
  69. // This object has expired, so don't return it.
  70. unset($results[$cid]);
  71. }
  72. }
  73. // Remove items from the referenced $cids array that we are returning,
  74. // per the comment in cache_get_multiple() in includes/cache.inc.
  75. $cids = array_diff($cids, array_keys($results));
  76. return $results;
  77. }
  78. /**
  79. * Garbage collection for get() and getMultiple().
  80. *
  81. * This functions check for session indicators to NOT use the cache. It
  82. * checks if the last cache clearing time in there expired and if so unsets
  83. * them, so anonymous users can get out of the "sessions" after some time, to
  84. * get fast delivered pages (e.g. from Varnish etc. again).
  85. */
  86. protected function garbageCollection() {
  87. // Clean-up the per-user cache expiration session data, so that the session
  88. // handler can properly clean-up the session data for anonymous users.
  89. if (isset($_SESSION['cache_flush'])) {
  90. foreach ($_SESSION['cache_flush'] as $bin => $timestamp) {
  91. $expire = REQUEST_TIME - variable_get('cache_lifetime_' . $this->bin, variable_get('cache_lifetime', 0));
  92. if ($timestamp < $expire) {
  93. unset($_SESSION['cache_flush'][$bin]);
  94. }
  95. }
  96. if (!$_SESSION['cache_flush']) {
  97. unset($_SESSION['cache_flush']);
  98. }
  99. }
  100. }
  101. /**
  102. * Checks if a retrieved cache item is valid.
  103. *
  104. * @param string $cid
  105. * The cache id of the item
  106. * @param mixed $cache
  107. * The cache item.
  108. *
  109. * @return bool
  110. * Whether the item is valid.
  111. */
  112. protected function valid($cid, $cache) {
  113. if ($cache) {
  114. $cache_tables = isset($_SESSION['cache_flush']) ? $_SESSION['cache_flush'] : NULL;
  115. // Items that have expired are invalid.
  116. if (isset($cache->expire) && $cache->expire !== CACHE_PERMANENT && $cache->expire <= REQUEST_TIME) {
  117. // If the memcache_stampede_protection variable is set, allow one
  118. // process to rebuild the cache entry while serving expired content to
  119. // the rest. Note that core happily returns expired cache items as valid
  120. // and relies on cron to expire them, but this is mostly reliant on its
  121. // use of CACHE_TEMPORARY which does not map well to memcache.
  122. // @see http://drupal.org/node/534092
  123. if (variable_get('memcache_stampede_protection', FALSE)) {
  124. // The process that acquires the lock will get a cache miss, all
  125. // others will get a cache hit.
  126. if ($this->lockInit() && $this->stampedeProtected($cid) && lock_acquire("memcache_$cid:$this->bin", variable_get('memcache_stampede_semaphore', 15))) {
  127. $cache = FALSE;
  128. }
  129. }
  130. else {
  131. $cache = FALSE;
  132. }
  133. }
  134. // Items created before the last full wildcard flush against this bin are
  135. // invalid.
  136. elseif (!isset($cache->created) || $cache->created <= $this->cache_flush) {
  137. $cache = FALSE;
  138. }
  139. // Items created before the last content flush on this bin i.e.
  140. // cache_clear_all() are invalid.
  141. elseif ($cache->expire != CACHE_PERMANENT && $cache->created + $this->cache_lifetime <= $this->cache_content_flush) {
  142. $cache = FALSE;
  143. }
  144. // Items cached before the cache was last flushed by the current user are
  145. // invalid.
  146. elseif ($cache->expire != CACHE_PERMANENT && is_array($cache_tables) && isset($cache_tables[$this->bin]) && $cache_tables[$this->bin] >= $cache->created) {
  147. // Cache item expired, return FALSE.
  148. $cache = FALSE;
  149. }
  150. // Temporary items created before the cache was last flushed by
  151. // cache_clear_all(NULL, $bin) are invalid.
  152. elseif (!empty($cache->temporary) && $cache->created + $this->cache_lifetime <= $this->cache_temporary_flush) {
  153. // CACHE_TEMPORARY item expired, return FALSE.
  154. $cache = FALSE;
  155. }
  156. // Finally, check for wildcard clears against this cid.
  157. else {
  158. if (!$this->wildcardValid($cid, $cache)) {
  159. $cache = FALSE;
  160. }
  161. }
  162. }
  163. // On cache misses, attempt to avoid stampedes when the
  164. // memcache_stampede_protection variable is enabled.
  165. if (!$cache) {
  166. if (variable_get('memcache_stampede_protection', FALSE) && $this->lockInit() && $this->stampedeProtected($cid) && !lock_acquire("memcache_$cid:$this->bin", variable_get('memcache_stampede_semaphore', 15))) {
  167. // Prevent any single request from waiting more than three times due to
  168. // stampede protection. By default this is a maximum total wait of 15
  169. // seconds. This accounts for two possibilities - a cache and lock miss
  170. // more than once for the same item. Or a cache and lock miss for
  171. // different items during the same request.
  172. // @todo: it would be better to base this on time waited rather than
  173. // number of waits, but the lock API does not currently provide this
  174. // information. Currently the limit will kick in for three waits of 25ms
  175. // or three waits of 5000ms.
  176. static $lock_count = 0;
  177. $lock_count++;
  178. if ($lock_count <= variable_get('memcache_stampede_wait_limit', 3)) {
  179. // The memcache_stampede_semaphore variable was used in previous
  180. // releases of memcache, but the max_wait variable was not, so by
  181. // default divide the semaphore value by 3 (5 seconds).
  182. lock_wait("memcache_$cid:$this->bin", variable_get('memcache_stampede_wait_time', 5));
  183. $cache = $this->get($cid);
  184. }
  185. }
  186. }
  187. $valid = (bool) $cache;
  188. if (variable_get('memcache_pagecache_header', FALSE) && ($this->bin == 'cache_page')) {
  189. // Per RFC-6648, don't start with X-
  190. $header = t('Drupal-Pagecache-Memcache: !status', array('!status' => $valid ? t('HIT') : t('MISS')));
  191. if ($valid) {
  192. $header .= t(', age=!age', array('!age' => REQUEST_TIME - $cache->created));
  193. }
  194. header ($header);
  195. }
  196. return $valid;
  197. }
  198. /**
  199. * Implements DrupalCacheInterface::set().
  200. */
  201. public function set($cid, $data, $expire = CACHE_PERMANENT) {
  202. $created_microtime = round(microtime(TRUE), 3);
  203. // Create new cache object.
  204. $cache = new stdClass();
  205. $cache->cid = $cid;
  206. $cache->data = is_object($data) ? clone $data : $data;
  207. $cache->created = REQUEST_TIME;
  208. $cache->created_microtime = $created_microtime;
  209. // Record the previous number of wildcard flushes affecting our cid.
  210. $cache->flushes = $this->wildcardFlushes($cid);
  211. if ($expire == CACHE_TEMPORARY) {
  212. // Convert CACHE_TEMPORARY (-1) into something that will live in memcache
  213. // until the next flush.
  214. $cache->expire = REQUEST_TIME + 2591999;
  215. // This is a temporary cache item.
  216. $cache->temporary = TRUE;
  217. }
  218. // Expire time is in seconds if less than 30 days, otherwise is a timestamp.
  219. elseif ($expire != CACHE_PERMANENT && $expire < 2592000) {
  220. // Expire is expressed in seconds, convert to the proper future timestamp
  221. // as expected in dmemcache_get().
  222. $cache->expire = REQUEST_TIME + $expire;
  223. $cache->temporary = FALSE;
  224. }
  225. else {
  226. $cache->expire = $expire;
  227. $cache->temporary = FALSE;
  228. }
  229. // Manually track the expire time in $cache->expire. When the object
  230. // expires, if stampede protection is enabled, it may be served while one
  231. // process rebuilds it. The ttl sent to memcache is set to the expire twice
  232. // as long into the future, this allows old items to be expired by memcache
  233. // rather than evicted along with a sufficient period for stampede
  234. // protection to continue to work.
  235. if ($cache->expire == CACHE_PERMANENT) {
  236. $memcache_expire = $cache->expire;
  237. }
  238. else {
  239. $memcache_expire = $cache->expire + (($cache->expire - REQUEST_TIME) * 2);
  240. }
  241. dmemcache_set($cid, $cache, $memcache_expire, $this->bin, $this->memcache);
  242. // Release lock if acquired in $this->valid().
  243. $lock = "memcache_$cid:$this->bin";
  244. if (variable_get('memcache_stampede_protection', FALSE) && isset($GLOBALS['locks'][$lock])) {
  245. lock_release("$lock");
  246. }
  247. }
  248. /**
  249. * Implements DrupalCacheInterface::clear().
  250. */
  251. public function clear($cid = NULL, $wildcard = FALSE) {
  252. if ($this->memcache === FALSE) {
  253. // No memcache connection.
  254. return;
  255. }
  256. // It is not possible to detect a cache_clear_all() call other than looking
  257. // at the backtrace unless http://drupal.org/node/81461 is added.
  258. $backtrace = debug_backtrace();
  259. if ($cid == MEMCACHE_CONTENT_CLEAR || (isset($backtrace[2]) && $backtrace[2]['function'] == 'cache_clear_all' && empty($backtrace[2]['args']))) {
  260. // Update the timestamp of the last global flushing of this bin. When
  261. // retrieving data from this bin, we will compare the cache creation
  262. // time minus the cache_flush time to the cache_lifetime to determine
  263. // whether or not the cached item is still valid.
  264. $this->cache_content_flush = time();
  265. $this->variable_set('cache_content_flush_' . $this->bin, $this->cache_content_flush);
  266. if (variable_get('cache_lifetime_' . $this->bin, variable_get('cache_lifetime', 0))) {
  267. // We store the time in the current user's session. We then simulate
  268. // that the cache was flushed for this user by not returning cached
  269. // data to this user that was cached before the timestamp.
  270. if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) {
  271. $cache_bins = $_SESSION['cache_flush'];
  272. }
  273. else {
  274. $cache_bins = array();
  275. }
  276. // Use time() rather than request time here for correctness.
  277. $cache_tables[$this->bin] = $this->cache_content_flush;
  278. $_SESSION['cache_flush'] = $cache_tables;
  279. }
  280. }
  281. if (empty($cid) || $wildcard === TRUE) {
  282. // system_cron() flushes all cache bins returned by hook_flush_caches()
  283. // with cache_clear_all(NULL, $bin); The expected behaviour in this case
  284. // is to perform garbage collection on expired cache items (which is
  285. // irrelevant to memcache) but also to remove all CACHE_TEMPORARY items.
  286. // @see https://api.drupal.org/api/drupal/includes!cache.inc/function/cache_clear_all/7
  287. if (!isset($cid)) {
  288. // Update the timestamp of the last CACHE_TEMPORARY clear. All
  289. // temporary cache items created before this will be invalidated.
  290. $this->cache_temporary_flush = time();
  291. $this->variable_set("cache_temporary_flush_$this->bin", $this->cache_temporary_flush);
  292. // Return early here as we do not want to register a wildcard flush.
  293. return;
  294. }
  295. elseif ($cid == '*') {
  296. $cid = '';
  297. }
  298. if (empty($cid)) {
  299. // Update the timestamp of the last global flushing of this bin. When
  300. // retrieving data from this bin, we will compare the cache creation
  301. // time minus the cache_flush time to the cache_lifetime to determine
  302. // whether or not the cached item is still valid.
  303. $this->cache_flush = time();
  304. $this->variable_set("cache_flush_$this->bin", $this->cache_flush);
  305. $this->flushed = min($this->cache_flush, time() - $this->cache_lifetime);
  306. if ($this->cache_lifetime) {
  307. // We store the time in the current user's session which is saved into
  308. // the sessions table by sess_write(). We then simulate that the
  309. // cache was flushed for this user by not returning cached data to
  310. // this user that was cached before the timestamp.
  311. if (isset($_SESSION['cache_flush']) && is_array($_SESSION['cache_flush'])) {
  312. $cache_bins = $_SESSION['cache_flush'];
  313. }
  314. else {
  315. $cache_bins = array();
  316. }
  317. $cache_bins[$this->bin] = $this->cache_flush;
  318. $_SESSION['cache_flush'] = $cache_bins;
  319. }
  320. }
  321. else {
  322. // Register a wildcard flush for current cid.
  323. $this->wildcards($cid, TRUE);
  324. }
  325. }
  326. else {
  327. $cids = is_array($cid) ? $cid : array($cid);
  328. foreach ($cids as $cid) {
  329. dmemcache_delete($cid, $this->bin, $this->memcache);
  330. }
  331. }
  332. }
  333. /**
  334. * Sum of all matching wildcards.
  335. *
  336. * Checking any single cache item's flush value against this single-value sum
  337. * tells us whether or not a new wildcard flush has affected the cached item.
  338. *
  339. * @param string $cid
  340. * The cache id to check.
  341. *
  342. * @return int
  343. * Sum of all matching wildcards for the given cache id.
  344. */
  345. protected function wildcardFlushes($cid) {
  346. return array_sum($this->wildcards($cid));
  347. }
  348. /**
  349. * Retrieves all matching wildcards for the given cache id.
  350. *
  351. * Utilize multiget to retrieve all possible wildcard matches, storing
  352. * statically so multiple cache requests for the same item on the same page
  353. * load doesn't add overhead.
  354. */
  355. protected function wildcards($cid, $flush = FALSE) {
  356. static $wildcards = array();
  357. $matching = array();
  358. if (!is_string($cid) && !is_int($cid)) {
  359. register_shutdown_function('watchdog', 'memcache', 'Invalid cache id received in memcache.inc wildcards() of type !type.', array('!type' => gettype($cid)), WATCHDOG_ERROR);
  360. return $matching;
  361. }
  362. $length = strlen($cid);
  363. if (isset($this->wildcard_flushes[$this->bin]) && is_array($this->wildcard_flushes[$this->bin])) {
  364. // Wildcard flushes per table are keyed by a substring equal to the
  365. // shortest wildcard clear on the table so far. So if the shortest
  366. // wildcard was "links:foo:", and the cid we're checking for is
  367. // "links:bar:bar", then the key will be "links:bar:".
  368. $keys = array_keys($this->wildcard_flushes[$this->bin]);
  369. $wildcard_length = strlen(reset($keys));
  370. $wildcard_key = substr($cid, 0, $wildcard_length);
  371. // Determine which lookups we need to perform to determine whether or not
  372. // our cid was impacted by a wildcard flush.
  373. $lookup = array();
  374. // Find statically cached wildcards, and determine possibly matching
  375. // wildcards for this cid based on a history of the lengths of past
  376. // valid wildcard flushes in this bin.
  377. if (isset($this->wildcard_flushes[$this->bin][$wildcard_key])) {
  378. foreach ($this->wildcard_flushes[$this->bin][$wildcard_key] as $flush_length => $timestamp) {
  379. if ($length >= $flush_length && $timestamp >= (REQUEST_TIME - $this->invalidate)) {
  380. $wildcard = '.wildcard-' . substr($cid, 0, $flush_length);
  381. if (isset($wildcards[$this->bin][$wildcard])) {
  382. $matching[$wildcard] = $wildcards[$this->bin][$wildcard];
  383. }
  384. else {
  385. $lookup[$wildcard] = $wildcard;
  386. }
  387. }
  388. }
  389. }
  390. // Do a multi-get to retrieve all possibly matching wildcard flushes.
  391. if (!empty($lookup)) {
  392. $values = dmemcache_get_multi($lookup, $this->bin, $this->memcache);
  393. if (is_array($values)) {
  394. // Prepare an array of matching wildcards.
  395. $matching = array_merge($matching, $values);
  396. // Store matches in the static cache.
  397. if (isset($wildcards[$this->bin])) {
  398. $wildcards[$this->bin] = array_merge($wildcards[$this->bin], $values);
  399. }
  400. else {
  401. $wildcards[$this->bin] = $values;
  402. }
  403. $lookup = array_diff_key($lookup, $values);
  404. }
  405. // Also store failed lookups in our static cache, so we don't have to
  406. // do repeat lookups on single page loads.
  407. foreach ($lookup as $key => $key) {
  408. $wildcards[$this->bin][$key] = 0;
  409. }
  410. }
  411. }
  412. if ($flush) {
  413. $key_length = $length;
  414. if (isset($this->wildcard_flushes[$this->bin])) {
  415. $keys = array_keys($this->wildcard_flushes[$this->bin]);
  416. $key_length = strlen(reset($keys));
  417. }
  418. $key = substr($cid, 0, $key_length);
  419. // Avoid too many calls to variable_set() by only recording a flush for
  420. // a fraction of the wildcard invalidation variable, per cid length.
  421. // Defaults to 28 / 4, or one week.
  422. if (!isset($this->wildcard_flushes[$this->bin][$key][$length]) || (REQUEST_TIME - $this->wildcard_flushes[$this->bin][$key][$length] > $this->invalidate / 4)) {
  423. // If there are more than 50 different wildcard keys for this bin
  424. // shorten the key by one, this should reduce variability by
  425. // an order of magnitude and ensure we don't use too much memory.
  426. if (isset($this->wildcard_flushes[$this->bin]) && count($this->wildcard_flushes[$this->bin]) > 50) {
  427. $key = substr($cid, 0, $key_length - 1);
  428. $length = strlen($key);
  429. }
  430. // If this is the shortest key length so far, we need to remove all
  431. // other wildcards lengths recorded so far for this bin and start
  432. // again. This is equivalent to a full cache flush for this table, but
  433. // it ensures the minimum possible number of wildcards are requested
  434. // along with cache consistency.
  435. if ($length < $key_length) {
  436. $this->wildcard_flushes[$this->bin] = array();
  437. $this->variable_set("cache_flush_$this->bin", time());
  438. $this->cache_flush = time();
  439. }
  440. $key = substr($cid, 0, $key_length);
  441. $this->wildcard_flushes[$this->bin][$key][$length] = REQUEST_TIME;
  442. variable_set('memcache_wildcard_flushes', $this->wildcard_flushes);
  443. }
  444. $key = '.wildcard-' . $cid;
  445. if (isset($wildcards[$this->bin][$key])) {
  446. $wildcards[$this->bin][$key]++;
  447. }
  448. else {
  449. $wildcards[$this->bin][$key] = 1;
  450. }
  451. dmemcache_set($key, $wildcards[$this->bin][$key], 0, $this->bin);
  452. }
  453. return $matching;
  454. }
  455. /**
  456. * Check if a wildcard flush has invalidated the current cached copy.
  457. */
  458. protected function wildcardValid($cid, $cache) {
  459. // Previously cached content won't have ->flushes defined. We could
  460. // force flush, but instead leave this up to the site admin.
  461. $flushes = isset($cache->flushes) ? (int) $cache->flushes : 0;
  462. if ($flushes < (int) $this->wildcardFlushes($cid)) {
  463. return FALSE;
  464. }
  465. return TRUE;
  466. }
  467. /**
  468. * Implements DrupalCacheInterface::isEmpty().
  469. */
  470. public function isEmpty() {
  471. // We do not know so err on the safe side?
  472. return FALSE;
  473. }
  474. /**
  475. * Helper function to load locking framework if not already loaded.
  476. *
  477. * @return bool
  478. * Whether the locking system was initialized successfully. This must always
  479. * return TRUE or throw an exception.
  480. */
  481. public function lockInit() {
  482. // On a cache miss when page_cache_without_database is enabled, we can end
  483. // up here without the lock system being initialized. Bootstrap drupal far
  484. // enough to load the lock system.
  485. if (!function_exists('lock_acquire')) {
  486. drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
  487. }
  488. if (!function_exists('lock_acquire')) {
  489. // Bootstrap failed, log error.
  490. register_shutdown_function('watchdog', 'memcache', 'Bootstrap failed in lockInit(), lock_acquire() is not available. (phase:!phase)', array('!phase' => drupal_get_bootstrap_phase()), WATCHDOG_ERROR);
  491. return FALSE;
  492. }
  493. return TRUE;
  494. }
  495. /**
  496. * Determines whether stampede protection is enabled for a given bin/cid.
  497. *
  498. * Memcache stampede protection is primarily designed to benefit the following
  499. * caching pattern: a miss on a cache_get for a specific cid is immediately
  500. * followed by a cache_set for that cid. In cases where this pattern is not
  501. * followed, stampede protection can be disabled to avoid long hanging locks.
  502. * For example, a cache miss in Drupal core's module_implements() won't
  503. * execute a cache_set until drupal_page_footer() calls
  504. * module_implements_write_cache() which can occur much later in page
  505. * generation.
  506. *
  507. * @param string $cid
  508. * The cache id of the data to retrieve.
  509. *
  510. * @return bool
  511. * Returns TRUE if stampede protection is enabled for that particular cache
  512. * bin/cid, otherwise FALSE.
  513. */
  514. protected function stampedeProtected($cid) {
  515. $ignore_settings = variable_get('memcache_stampede_protection_ignore', array(
  516. // Disable stampede protection for specific cids in 'cache_bootstrap'.
  517. 'cache_bootstrap' => array(
  518. // The module_implements cache is written after finishing the request.
  519. 'module_implements',
  520. // Variables have their own lock protection.
  521. 'variables',
  522. // Delayed set.
  523. 'lookup_cache',
  524. // Both schema and the theme_registry uses DrupalCacheArray, which sets
  525. // the cache entry with a class destructor.
  526. 'schema:runtime:*',
  527. 'theme_registry:runtime:*',
  528. // Not written until the end of drupal_page_footer()
  529. '_drupal_file_scan_cache',
  530. ),
  531. // Disable stampede protection for cid prefix in 'cache'.
  532. 'cache' => array(
  533. // I18n uses a class destructor to set the cache.
  534. 'i18n:string:*',
  535. ),
  536. // Delayed set.
  537. 'cache_path',
  538. // Disable stampede protection for the contrib cache_rules bin as recent
  539. // versions of the rules module provides its own stampede protection.
  540. 'cache_rules',
  541. ));
  542. // Support ignoring an entire bin.
  543. if (in_array($this->bin, $ignore_settings)) {
  544. return FALSE;
  545. }
  546. // Support ignoring by cids.
  547. if (isset($ignore_settings[$this->bin])) {
  548. // Support ignoring specific cids.
  549. if (in_array($cid, $ignore_settings[$this->bin])) {
  550. return FALSE;
  551. }
  552. // Support ignoring cids starting with a suffix.
  553. foreach ($ignore_settings[$this->bin] as $ignore) {
  554. $split = explode('*', $ignore);
  555. if (count($split) > 1 && strpos($cid, $split[0]) === 0) {
  556. return FALSE;
  557. }
  558. }
  559. }
  560. return TRUE;
  561. }
  562. /**
  563. * Helper function to reload variables.
  564. *
  565. * This is used by the tests to verify that the cache object used the correct
  566. * settings.
  567. */
  568. public function reloadVariables() {
  569. $this->wildcard_flushes = variable_get('memcache_wildcard_flushes', array());
  570. $this->invalidate = variable_get('memcache_wildcard_invalidate', MEMCACHE_WILDCARD_INVALIDATE);
  571. $this->cache_lifetime = variable_get('cache_lifetime_' . $this->bin, variable_get('cache_lifetime', 0));
  572. $this->cache_flush = variable_get('cache_flush_' . $this->bin);
  573. $this->cache_content_flush = variable_get('cache_content_flush_' . $this->bin, 0);
  574. $this->cache_temporary_flush = variable_get('cache_temporary_flush_' . $this->bin, 0);
  575. $this->flushed = min($this->cache_flush, REQUEST_TIME - $this->cache_lifetime);
  576. }
  577. /**
  578. * Re-implementation of variable_set() that writes through instead of clearing.
  579. */
  580. public function variable_set($name, $value) {
  581. global $conf;
  582. // If the value appears unchanged, do nothing.
  583. if (isset($conf[$name]) && $conf[$name] === $value) {
  584. return;
  585. }
  586. // When lots of writes happen in a short period of time db_merge can throw
  587. // errors. This should only happen if another request has written the
  588. // variable first, so we catch the error to prevent a fatal error.
  589. try {
  590. db_merge('variable')
  591. ->key(array('name' => $name))
  592. ->fields(array('value' => serialize($value)))
  593. ->execute();
  594. }
  595. catch (Exception $e) {
  596. // We can safely ignore the error, since it's likely a cache flush
  597. // timestamp which should still be accurate.
  598. }
  599. // If the variables are cached, get a fresh copy, update with the new value
  600. // and set it again.
  601. if ($cached = cache_get('variables', 'cache_bootstrap')) {
  602. $variables = $cached->data;
  603. $variables[$name] = $value;
  604. cache_set('variables', $variables, 'cache_bootstrap');
  605. }
  606. // If the variables aren't cached, there's no need to do anything.
  607. $conf[$name] = $value;
  608. }
  609. }