Cache.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. <?php
  2. /**
  3. * Because those objects will be spawned during boostrap all its configuration
  4. * must be set in the settings.php file.
  5. *
  6. * You will find the driver specific implementation in the Redis_Cache_*
  7. * classes as they may differ in how the API handles transaction, pipelining
  8. * and return values.
  9. */
  10. class Redis_Cache
  11. implements DrupalCacheInterface
  12. {
  13. /**
  14. * Default lifetime for permanent items.
  15. * Approximatively 1 year.
  16. */
  17. const LIFETIME_PERM_DEFAULT = 31536000;
  18. /**
  19. * Uses EVAL scripts to flush data when called
  20. *
  21. * This remains the default behavior and is safe until you use a single
  22. * Redis server instance and its version is >= 2.6 (older version don't
  23. * support EVAL).
  24. */
  25. const FLUSH_NORMAL = 0;
  26. /**
  27. * This mode is tailored for sharded Redis servers instances usage: it
  28. * will never delete entries but only mark the latest flush timestamp
  29. * into one of the servers in the shard. It will proceed to delete on
  30. * read single entries when invalid entries are being loaded.
  31. */
  32. const FLUSH_SHARD = 3;
  33. /**
  34. * Same as the one above, plus attempt to do pipelining when possible.
  35. *
  36. * This is supposed to work with sharding proxies that supports
  37. * pipelining themselves, such as Twemproxy.
  38. */
  39. const FLUSH_SHARD_WITH_PIPELINING = 4;
  40. /**
  41. * Computed keys are let's say arround 60 characters length due to
  42. * key prefixing, which makes 1,000 keys DEL command to be something
  43. * arround 50,000 bytes length: this is huge and may not pass into
  44. * Redis, let's split this off.
  45. * Some recommend to never get higher than 1,500 bytes within the same
  46. * command which makes us forced to split this at a very low threshold:
  47. * 20 seems a safe value here (1,280 average length).
  48. */
  49. const KEY_THRESHOLD = 20;
  50. /**
  51. * @var Redis_Cache_BackendInterface
  52. */
  53. protected $backend;
  54. /**
  55. * @var string
  56. */
  57. protected $bin;
  58. /**
  59. * When the global 'cache_lifetime' Drupal variable is set to a value, the
  60. * cache backends should not expire temporary entries by themselves per
  61. * Drupal signature. Volatile items will be dropped accordingly to their
  62. * set lifetime.
  63. *
  64. * @var boolean
  65. */
  66. protected $allowTemporaryFlush = true;
  67. /**
  68. * When in shard mode, the backend cannot proceed to multiple keys
  69. * operations, and won't delete keys on flush calls.
  70. *
  71. * @var boolean
  72. */
  73. protected $isSharded = false;
  74. /**
  75. * When in shard mode, the proxy may or may not support pipelining,
  76. * Twemproxy is known to support it.
  77. *
  78. * @var boolean
  79. */
  80. protected $allowPipeline = false;
  81. /**
  82. * Default TTL for CACHE_PERMANENT items.
  83. *
  84. * See "Default lifetime for permanent items" section of README.txt
  85. * file for a comprehensive explaination of why this exists.
  86. *
  87. * @var int
  88. */
  89. protected $permTtl = self::LIFETIME_PERM_DEFAULT;
  90. /**
  91. * Maximum TTL for this bin from Drupal configuration.
  92. *
  93. * @var int
  94. */
  95. protected $maxTtl = 0;
  96. /**
  97. * Flush permanent and volatile cached values
  98. *
  99. * @var string[]
  100. * First value is permanent latest flush time and second value
  101. * is volatile latest flush time
  102. */
  103. protected $flushCache = null;
  104. /**
  105. * Is this bin in shard mode
  106. *
  107. * @return boolean
  108. */
  109. public function isSharded()
  110. {
  111. return $this->isSharded;
  112. }
  113. /**
  114. * Does this bin allow pipelining through sharded environment
  115. *
  116. * @return boolean
  117. */
  118. public function allowPipeline()
  119. {
  120. return $this->allowPipeline;
  121. }
  122. /**
  123. * Does this bin allow temporary item flush
  124. *
  125. * @return boolean
  126. */
  127. public function allowTemporaryFlush()
  128. {
  129. return $this->allowTemporaryFlush;
  130. }
  131. /**
  132. * Get TTL for CACHE_PERMANENT items.
  133. *
  134. * @return int
  135. * Lifetime in seconds.
  136. */
  137. public function getPermTtl()
  138. {
  139. return $this->permTtl;
  140. }
  141. /**
  142. * Get maximum TTL for all items.
  143. *
  144. * @return int
  145. * Lifetime in seconds.
  146. */
  147. public function getMaxTtl()
  148. {
  149. return $this->maxTtl;
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function __construct($bin)
  155. {
  156. $this->bin = $bin;
  157. $className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_CACHE);
  158. $this->backend = new $className(Redis_Client::getClient(), $bin, Redis_Client::getDefaultPrefix($bin));
  159. $this->refreshCapabilities();
  160. $this->refreshPermTtl();
  161. $this->refreshMaxTtl();
  162. }
  163. /**
  164. * Find from Drupal variables the clear mode.
  165. */
  166. public function refreshCapabilities()
  167. {
  168. if (0 < variable_get('cache_lifetime', 0)) {
  169. // Per Drupal default behavior, when the 'cache_lifetime' variable
  170. // is set we must not flush any temporary items since they have a
  171. // life time.
  172. $this->allowTemporaryFlush = false;
  173. }
  174. if (null !== ($mode = variable_get('redis_flush_mode', null))) {
  175. $mode = (int)$mode;
  176. } else {
  177. $mode = self::FLUSH_NORMAL;
  178. }
  179. $this->isSharded = self::FLUSH_SHARD === $mode || self::FLUSH_SHARD_WITH_PIPELINING === $mode;
  180. $this->allowPipeline = self::FLUSH_SHARD !== $mode;
  181. }
  182. /**
  183. * Find from Drupal variables the right permanent items TTL.
  184. */
  185. protected function refreshPermTtl()
  186. {
  187. $ttl = null;
  188. if (null === ($ttl = variable_get('redis_perm_ttl_' . $this->bin, null))) {
  189. if (null === ($ttl = variable_get('redis_perm_ttl', null))) {
  190. $ttl = self::LIFETIME_PERM_DEFAULT;
  191. }
  192. }
  193. if ($ttl === (int)$ttl) {
  194. $this->permTtl = $ttl;
  195. } else {
  196. if ($iv = DateInterval::createFromDateString($ttl)) {
  197. // http://stackoverflow.com/questions/14277611/convert-dateinterval-object-to-seconds-in-php
  198. $this->permTtl = ($iv->y * 31536000 + $iv->m * 2592000 + $iv->d * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s);
  199. } else {
  200. // Sorry but we have to log this somehow.
  201. trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl));
  202. $this->permTtl = self::LIFETIME_PERM_DEFAULT;
  203. }
  204. }
  205. }
  206. /**
  207. * Find from Drupal variables the maximum cache lifetime.
  208. */
  209. public function refreshMaxTtl()
  210. {
  211. // And now cache lifetime. Be aware we exclude negative values
  212. // considering those are Drupal misconfiguration.
  213. $maxTtl = variable_get('cache_lifetime', 0);
  214. if (0 < $maxTtl) {
  215. if ($maxTtl < $this->permTtl) {
  216. $this->maxTtl = $maxTtl;
  217. } else {
  218. $this->maxTtl = $this->permTtl;
  219. }
  220. } else if ($this->permTtl) {
  221. $this->maxTtl = $this->permTtl;
  222. }
  223. }
  224. /**
  225. * Set last flush time
  226. *
  227. * @param string $permanent
  228. * @param string $volatile
  229. */
  230. public function setLastFlushTime($permanent = false, $volatile = false)
  231. {
  232. // Here we need to fetch absolute values from backend, to avoid
  233. // concurrency problems and ensure data validity.
  234. list($flushPerm, $flushVolatile) = $this->backend->getLastFlushTime();
  235. $checksum = $this->getValidChecksum(
  236. max(array(
  237. $flushPerm,
  238. $flushVolatile,
  239. $permanent,
  240. time(),
  241. ))
  242. );
  243. if ($permanent) {
  244. $this->backend->setLastFlushTimeFor($checksum, false);
  245. $this->backend->setLastFlushTimeFor($checksum, true);
  246. $this->flushCache = array($checksum, $checksum);
  247. } else if ($volatile) {
  248. $this->backend->setLastFlushTimeFor($checksum, true);
  249. $this->flushCache = array($flushPerm, $checksum);
  250. }
  251. }
  252. /**
  253. * Get latest flush time
  254. *
  255. * @return string[]
  256. * First value is the latest flush time for permanent entries checksum,
  257. * second value is the latest flush time for volatile entries checksum.
  258. */
  259. public function getLastFlushTime()
  260. {
  261. if (!$this->flushCache) {
  262. $this->flushCache = $this->backend->getLastFlushTime();
  263. }
  264. // At the very first hit, we might not have the timestamps set, thus
  265. // we need to create them to avoid our entry being considered as
  266. // invalid
  267. if (!$this->flushCache[0]) {
  268. $this->setLastFlushTime(true, true);
  269. } else if (!$this->flushCache[1]) {
  270. $this->setLastFlushTime(false, true);
  271. }
  272. return $this->flushCache;
  273. }
  274. /**
  275. * Create cache entry
  276. *
  277. * @param string $cid
  278. * @param mixed $data
  279. *
  280. * @return array
  281. */
  282. protected function createEntryHash($cid, $data, $expire = CACHE_PERMANENT)
  283. {
  284. list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
  285. if (CACHE_TEMPORARY === $expire) {
  286. $validityThreshold = max(array($flushVolatile, $flushPerm));
  287. } else {
  288. $validityThreshold = $flushPerm;
  289. }
  290. $time = $this->getValidChecksum($validityThreshold);
  291. $hash = array(
  292. 'cid' => $cid,
  293. 'created' => $time,
  294. 'expire' => $expire,
  295. );
  296. // Let Redis handle the data types itself.
  297. if (!is_string($data)) {
  298. $hash['data'] = serialize($data);
  299. $hash['serialized'] = 1;
  300. } else {
  301. $hash['data'] = $data;
  302. $hash['serialized'] = 0;
  303. }
  304. return $hash;
  305. }
  306. /**
  307. * Expand cache entry from fetched data
  308. *
  309. * @param array $values
  310. * Raw values fetched from Redis server data
  311. *
  312. * @return array
  313. * Or FALSE if entry is invalid
  314. */
  315. protected function expandEntry(array $values, $flushPerm, $flushVolatile)
  316. {
  317. // Check for entry being valid.
  318. if (empty($values['cid'])) {
  319. return;
  320. }
  321. // This ensures backward compatibility with older version of
  322. // this module's data still stored in Redis.
  323. if (isset($values['expire'])) {
  324. $expire = (int)$values['expire'];
  325. // Ensure the entry is valid and have not expired.
  326. if ($expire !== CACHE_PERMANENT && $expire !== CACHE_TEMPORARY && $expire <= time()) {
  327. return false;
  328. }
  329. }
  330. // Ensure the entry does not predate the last flush time.
  331. if ($this->allowTemporaryFlush && !empty($values['volatile'])) {
  332. $validityThreshold = max(array($flushPerm, $flushVolatile));
  333. } else {
  334. $validityThreshold = $flushPerm;
  335. }
  336. if ($values['created'] <= $validityThreshold) {
  337. return false;
  338. }
  339. $entry = (object)$values;
  340. // Reduce the checksum to the real timestamp part
  341. $entry->created = (int)$entry->created;
  342. if ($entry->serialized) {
  343. $entry->data = unserialize($entry->data);
  344. }
  345. return $entry;
  346. }
  347. /**
  348. * {@inheritdoc}
  349. */
  350. public function get($cid)
  351. {
  352. $values = $this->backend->get($cid);
  353. if (empty($values)) {
  354. return false;
  355. }
  356. list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
  357. $entry = $this->expandEntry($values, $flushPerm, $flushVolatile);
  358. if (!$entry) { // This entry exists but is invalid.
  359. $this->backend->delete($cid);
  360. return false;
  361. }
  362. return $entry;
  363. }
  364. /**
  365. * {@inheritdoc}
  366. */
  367. public function getMultiple(&$cids)
  368. {
  369. $ret = array();
  370. $delete = array();
  371. if (!$this->allowPipeline) {
  372. $entries = array();
  373. foreach ($cids as $cid) {
  374. if ($entry = $this->backend->get($cid)) {
  375. $entries[$cid] = $entry;
  376. }
  377. }
  378. } else {
  379. $entries = $this->backend->getMultiple($cids);
  380. }
  381. list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
  382. foreach ($cids as $key => $cid) {
  383. if (!empty($entries[$cid])) {
  384. $entry = $this->expandEntry($entries[$cid], $flushPerm, $flushVolatile);
  385. } else {
  386. $entry = null;
  387. }
  388. if (empty($entry)) {
  389. $delete[] = $cid;
  390. } else {
  391. $ret[$cid] = $entry;
  392. unset($cids[$key]);
  393. }
  394. }
  395. if (!empty($delete)) {
  396. if ($this->allowPipeline) {
  397. foreach ($delete as $id) {
  398. $this->backend->delete($id);
  399. }
  400. } else {
  401. $this->backend->deleteMultiple($delete);
  402. }
  403. }
  404. return $ret;
  405. }
  406. /**
  407. * {@inheritdoc}
  408. */
  409. public function set($cid, $data, $expire = CACHE_PERMANENT)
  410. {
  411. $hash = $this->createEntryHash($cid, $data, $expire);
  412. $maxTtl = $this->getMaxTtl();
  413. switch ($expire) {
  414. case CACHE_PERMANENT:
  415. $this->backend->set($cid, $hash, $maxTtl, false);
  416. break;
  417. case CACHE_TEMPORARY:
  418. $this->backend->set($cid, $hash, $maxTtl, true);
  419. break;
  420. default:
  421. $ttl = $expire - time();
  422. // Ensure $expire consistency
  423. if ($ttl <= 0) {
  424. // Entry has already expired, but we may have a stalled
  425. // older cache entry remaining there, ensure it wont
  426. // happen by doing a preventive delete
  427. $this->backend->delete($cid);
  428. } else {
  429. if ($maxTtl && $maxTtl < $ttl) {
  430. $ttl = $maxTtl;
  431. }
  432. $this->backend->set($cid, $hash, $ttl, false);
  433. }
  434. break;
  435. }
  436. }
  437. /**
  438. * {@inheritdoc}
  439. */
  440. public function clear($cid = null, $wildcard = false)
  441. {
  442. if (null === $cid && !$wildcard) {
  443. // Drupal asked for volatile entries flush, this will happen
  444. // during cron run, mostly
  445. $this->setLastFlushTime(false, true);
  446. if (!$this->isSharded && $this->allowTemporaryFlush) {
  447. $this->backend->flushVolatile();
  448. }
  449. } else if ($wildcard) {
  450. if (empty($cid)) {
  451. // This seems to be an error, just do nothing.
  452. return;
  453. }
  454. if ('*' === $cid) {
  455. // Use max() to ensure we invalidate both correctly
  456. $this->setLastFlushTime(true);
  457. if (!$this->isSharded) {
  458. $this->backend->flush();
  459. }
  460. } else {
  461. if (!$this->isSharded) {
  462. $this->backend->deleteByPrefix($cid);
  463. } else {
  464. // @todo This needs a map algorithm the same way memcache
  465. // module implemented it for invalidity by prefixes. This
  466. // is a very stupid fallback
  467. $this->setLastFlushTime(true);
  468. }
  469. }
  470. } else if (is_array($cid)) {
  471. $this->backend->deleteMultiple($cid);
  472. } else {
  473. $this->backend->delete($cid);
  474. }
  475. }
  476. public function isEmpty()
  477. {
  478. return false;
  479. }
  480. /**
  481. * From the given timestamp build an incremental safe time-based identifier.
  482. *
  483. * Due to potential accidental cache wipes, when a server goes down in the
  484. * cluster or when a server triggers its LRU algorithm wipe-out, keys that
  485. * matches flush or tags checksum might be dropped.
  486. *
  487. * Per default, each new inserted tag will trigger a checksum computation to
  488. * be stored in the Redis server as a timestamp. In order to ensure a checksum
  489. * validity a simple comparison between the tag checksum and the cache entry
  490. * checksum will tell us if the entry pre-dates the current checksum or not,
  491. * thus telling us its state. The main problem we experience is that Redis
  492. * is being so fast it is able to create and drop entries at same second,
  493. * sometime even the same micro second. The only safe way to avoid conflicts
  494. * is to checksum using an arbitrary computed number (a sequence).
  495. *
  496. * Drupal core does exactly this thus tags checksums are additions of each tag
  497. * individual checksum; each tag checksum is a independent arbitrary serial
  498. * that gets incremented starting with 0 (no invalidation done yet) to n (n
  499. * invalidations) which grows over time. This way the checksum computation
  500. * always rises and we have a sensible default that works in all cases.
  501. *
  502. * This model works as long as you can ensure consistency for the serial
  503. * storage over time. Nevertheless, as explained upper, in our case this
  504. * serial might be dropped at some point for various valid technical reasons:
  505. * if we start over to 0, we may accidentally compute a checksum which already
  506. * existed in the past and make invalid entries turn back to valid again.
  507. *
  508. * In order to prevent this behavior, using a timestamp as part of the serial
  509. * ensures that we won't experience this problem in a time range wider than a
  510. * single second, which is safe enough for us. But using timestamp creates a
  511. * new problem: Redis is so fast that we can set or delete hundreds of entries
  512. * easily during the same second: an entry created then invalidated the same
  513. * second will create false positives (entry is being considered as valid) -
  514. * note that depending on the check algorithm, false negative may also happen
  515. * the same way. Therefore we need to have an abitrary serial value to be
  516. * incremented in order to enforce our checks to be more strict.
  517. *
  518. * The solution to both the first (the need for a time based checksum in case
  519. * of checksum data being dropped) and the second (the need to have an
  520. * arbitrary predictible serial value to avoid false positives or negatives)
  521. * we are combining the two: every checksum will be built this way:
  522. *
  523. * UNIXTIMESTAMP.SERIAL
  524. *
  525. * For example:
  526. *
  527. * 1429789217.017
  528. *
  529. * will reprensent the 17th invalidation of the 1429789217 exact second which
  530. * happened while writing this documentation. The next tag being invalidated
  531. * the same second will then have this checksum:
  532. *
  533. * 1429789217.018
  534. *
  535. * And so on...
  536. *
  537. * In order to make it consitent with PHP string and float comparison we need
  538. * to set fixed precision over the decimal, and store as a string to avoid
  539. * possible float precision problems when comparing.
  540. *
  541. * This algorithm is not fully failsafe, but allows us to proceed to 1000
  542. * operations on the same checksum during the same second, which is a
  543. * sufficiently great value to reduce the conflict probability to almost
  544. * zero for most uses cases.
  545. *
  546. * @param int|string $timestamp
  547. * "TIMESTAMP[.INCREMENT]" string
  548. *
  549. * @return string
  550. * The next "TIMESTAMP.INCREMENT" string.
  551. */
  552. public function getNextIncrement($timestamp = null)
  553. {
  554. if (!$timestamp) {
  555. return time() . '.000';
  556. }
  557. if (false !== ($pos = strpos($timestamp, '.'))) {
  558. $inc = substr($timestamp, $pos + 1, 3);
  559. return ((int)$timestamp) . '.' . str_pad($inc + 1, 3, '0', STR_PAD_LEFT);
  560. }
  561. return $timestamp . '.000';
  562. }
  563. /**
  564. * Get valid checksum
  565. *
  566. * @param int|string $previous
  567. * "TIMESTAMP[.INCREMENT]" string
  568. *
  569. * @return string
  570. * The next "TIMESTAMP.INCREMENT" string.
  571. *
  572. * @see Redis_Cache::getNextIncrement()
  573. */
  574. public function getValidChecksum($previous = null)
  575. {
  576. if (time() === (int)$previous) {
  577. return $this->getNextIncrement($previous);
  578. } else {
  579. return $this->getNextIncrement();
  580. }
  581. }
  582. }