123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655 |
- <?php
- /**
- * Because those objects will be spawned during boostrap all its configuration
- * must be set in the settings.php file.
- *
- * You will find the driver specific implementation in the Redis_Cache_*
- * classes as they may differ in how the API handles transaction, pipelining
- * and return values.
- */
- class Redis_Cache
- implements DrupalCacheInterface
- {
- /**
- * Default lifetime for permanent items.
- * Approximatively 1 year.
- */
- const LIFETIME_PERM_DEFAULT = 31536000;
- /**
- * Uses EVAL scripts to flush data when called
- *
- * This remains the default behavior and is safe until you use a single
- * Redis server instance and its version is >= 2.6 (older version don't
- * support EVAL).
- */
- const FLUSH_NORMAL = 0;
- /**
- * This mode is tailored for sharded Redis servers instances usage: it
- * will never delete entries but only mark the latest flush timestamp
- * into one of the servers in the shard. It will proceed to delete on
- * read single entries when invalid entries are being loaded.
- */
- const FLUSH_SHARD = 3;
- /**
- * Same as the one above, plus attempt to do pipelining when possible.
- *
- * This is supposed to work with sharding proxies that supports
- * pipelining themselves, such as Twemproxy.
- */
- const FLUSH_SHARD_WITH_PIPELINING = 4;
- /**
- * Computed keys are let's say arround 60 characters length due to
- * key prefixing, which makes 1,000 keys DEL command to be something
- * arround 50,000 bytes length: this is huge and may not pass into
- * Redis, let's split this off.
- * Some recommend to never get higher than 1,500 bytes within the same
- * command which makes us forced to split this at a very low threshold:
- * 20 seems a safe value here (1,280 average length).
- */
- const KEY_THRESHOLD = 20;
- /**
- * @var Redis_Cache_BackendInterface
- */
- protected $backend;
- /**
- * @var string
- */
- protected $bin;
- /**
- * When the global 'cache_lifetime' Drupal variable is set to a value, the
- * cache backends should not expire temporary entries by themselves per
- * Drupal signature. Volatile items will be dropped accordingly to their
- * set lifetime.
- *
- * @var boolean
- */
- protected $allowTemporaryFlush = true;
- /**
- * When in shard mode, the backend cannot proceed to multiple keys
- * operations, and won't delete keys on flush calls.
- *
- * @var boolean
- */
- protected $isSharded = false;
- /**
- * When in shard mode, the proxy may or may not support pipelining,
- * Twemproxy is known to support it.
- *
- * @var boolean
- */
- protected $allowPipeline = false;
- /**
- * Default TTL for CACHE_PERMANENT items.
- *
- * See "Default lifetime for permanent items" section of README.txt
- * file for a comprehensive explaination of why this exists.
- *
- * @var int
- */
- protected $permTtl = self::LIFETIME_PERM_DEFAULT;
- /**
- * Maximum TTL for this bin from Drupal configuration.
- *
- * @var int
- */
- protected $maxTtl = 0;
- /**
- * Flush permanent and volatile cached values
- *
- * @var string[]
- * First value is permanent latest flush time and second value
- * is volatile latest flush time
- */
- protected $flushCache = null;
- /**
- * Is this bin in shard mode
- *
- * @return boolean
- */
- public function isSharded()
- {
- return $this->isSharded;
- }
- /**
- * Does this bin allow pipelining through sharded environment
- *
- * @return boolean
- */
- public function allowPipeline()
- {
- return $this->allowPipeline;
- }
- /**
- * Does this bin allow temporary item flush
- *
- * @return boolean
- */
- public function allowTemporaryFlush()
- {
- return $this->allowTemporaryFlush;
- }
- /**
- * Get TTL for CACHE_PERMANENT items.
- *
- * @return int
- * Lifetime in seconds.
- */
- public function getPermTtl()
- {
- return $this->permTtl;
- }
- /**
- * Get maximum TTL for all items.
- *
- * @return int
- * Lifetime in seconds.
- */
- public function getMaxTtl()
- {
- return $this->maxTtl;
- }
- /**
- * {@inheritdoc}
- */
- public function __construct($bin)
- {
- $this->bin = $bin;
- $className = Redis_Client::getClass(Redis_Client::REDIS_IMPL_CACHE);
- $this->backend = new $className(Redis_Client::getClient(), $bin, Redis_Client::getDefaultPrefix($bin));
- $this->refreshCapabilities();
- $this->refreshPermTtl();
- $this->refreshMaxTtl();
- }
- /**
- * Find from Drupal variables the clear mode.
- */
- public function refreshCapabilities()
- {
- if (0 < variable_get('cache_lifetime', 0)) {
- // Per Drupal default behavior, when the 'cache_lifetime' variable
- // is set we must not flush any temporary items since they have a
- // life time.
- $this->allowTemporaryFlush = false;
- }
- if (null !== ($mode = variable_get('redis_flush_mode', null))) {
- $mode = (int)$mode;
- } else {
- $mode = self::FLUSH_NORMAL;
- }
- $this->isSharded = self::FLUSH_SHARD === $mode || self::FLUSH_SHARD_WITH_PIPELINING === $mode;
- $this->allowPipeline = self::FLUSH_SHARD !== $mode;
- }
- /**
- * Find from Drupal variables the right permanent items TTL.
- */
- protected function refreshPermTtl()
- {
- $ttl = null;
- if (null === ($ttl = variable_get('redis_perm_ttl_' . $this->bin, null))) {
- if (null === ($ttl = variable_get('redis_perm_ttl', null))) {
- $ttl = self::LIFETIME_PERM_DEFAULT;
- }
- }
- if ($ttl === (int)$ttl) {
- $this->permTtl = $ttl;
- } else {
- if ($iv = DateInterval::createFromDateString($ttl)) {
- // http://stackoverflow.com/questions/14277611/convert-dateinterval-object-to-seconds-in-php
- $this->permTtl = ($iv->y * 31536000 + $iv->m * 2592000 + $iv->d * 86400 + $iv->h * 3600 + $iv->i * 60 + $iv->s);
- } else {
- // Sorry but we have to log this somehow.
- trigger_error(sprintf("Parsed TTL '%s' has an invalid value: switching to default", $ttl));
- $this->permTtl = self::LIFETIME_PERM_DEFAULT;
- }
- }
- }
- /**
- * Find from Drupal variables the maximum cache lifetime.
- */
- public function refreshMaxTtl()
- {
- // And now cache lifetime. Be aware we exclude negative values
- // considering those are Drupal misconfiguration.
- $maxTtl = variable_get('cache_lifetime', 0);
- if (0 < $maxTtl) {
- if ($maxTtl < $this->permTtl) {
- $this->maxTtl = $maxTtl;
- } else {
- $this->maxTtl = $this->permTtl;
- }
- } else if ($this->permTtl) {
- $this->maxTtl = $this->permTtl;
- }
- }
- /**
- * Set last flush time
- *
- * @param string $permanent
- * @param string $volatile
- */
- public function setLastFlushTime($permanent = false, $volatile = false)
- {
- // Here we need to fetch absolute values from backend, to avoid
- // concurrency problems and ensure data validity.
- list($flushPerm, $flushVolatile) = $this->backend->getLastFlushTime();
- $checksum = $this->getValidChecksum(
- max(array(
- $flushPerm,
- $flushVolatile,
- $permanent,
- time(),
- ))
- );
- if ($permanent) {
- $this->backend->setLastFlushTimeFor($checksum, false);
- $this->backend->setLastFlushTimeFor($checksum, true);
- $this->flushCache = array($checksum, $checksum);
- } else if ($volatile) {
- $this->backend->setLastFlushTimeFor($checksum, true);
- $this->flushCache = array($flushPerm, $checksum);
- }
- }
- /**
- * Get latest flush time
- *
- * @return string[]
- * First value is the latest flush time for permanent entries checksum,
- * second value is the latest flush time for volatile entries checksum.
- */
- public function getLastFlushTime()
- {
- if (!$this->flushCache) {
- $this->flushCache = $this->backend->getLastFlushTime();
- }
- // At the very first hit, we might not have the timestamps set, thus
- // we need to create them to avoid our entry being considered as
- // invalid
- if (!$this->flushCache[0]) {
- $this->setLastFlushTime(true, true);
- } else if (!$this->flushCache[1]) {
- $this->setLastFlushTime(false, true);
- }
- return $this->flushCache;
- }
- /**
- * Create cache entry
- *
- * @param string $cid
- * @param mixed $data
- *
- * @return array
- */
- protected function createEntryHash($cid, $data, $expire = CACHE_PERMANENT)
- {
- list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
- if (CACHE_TEMPORARY === $expire) {
- $validityThreshold = max(array($flushVolatile, $flushPerm));
- } else {
- $validityThreshold = $flushPerm;
- }
- $time = $this->getValidChecksum($validityThreshold);
- $hash = array(
- 'cid' => $cid,
- 'created' => $time,
- 'expire' => $expire,
- );
- // Let Redis handle the data types itself.
- if (!is_string($data)) {
- $hash['data'] = serialize($data);
- $hash['serialized'] = 1;
- } else {
- $hash['data'] = $data;
- $hash['serialized'] = 0;
- }
- return $hash;
- }
- /**
- * Expand cache entry from fetched data
- *
- * @param array $values
- * Raw values fetched from Redis server data
- *
- * @return array
- * Or FALSE if entry is invalid
- */
- protected function expandEntry(array $values, $flushPerm, $flushVolatile)
- {
- // Check for entry being valid.
- if (empty($values['cid'])) {
- return;
- }
- // This ensures backward compatibility with older version of
- // this module's data still stored in Redis.
- if (isset($values['expire'])) {
- $expire = (int)$values['expire'];
- // Ensure the entry is valid and have not expired.
- if ($expire !== CACHE_PERMANENT && $expire !== CACHE_TEMPORARY && $expire <= time()) {
- return false;
- }
- }
- // Ensure the entry does not predate the last flush time.
- if ($this->allowTemporaryFlush && !empty($values['volatile'])) {
- $validityThreshold = max(array($flushPerm, $flushVolatile));
- } else {
- $validityThreshold = $flushPerm;
- }
- if ($values['created'] <= $validityThreshold) {
- return false;
- }
- $entry = (object)$values;
- // Reduce the checksum to the real timestamp part
- $entry->created = (int)$entry->created;
- if ($entry->serialized) {
- $entry->data = unserialize($entry->data);
- }
- return $entry;
- }
- /**
- * {@inheritdoc}
- */
- public function get($cid)
- {
- $values = $this->backend->get($cid);
- if (empty($values)) {
- return false;
- }
- list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
- $entry = $this->expandEntry($values, $flushPerm, $flushVolatile);
- if (!$entry) { // This entry exists but is invalid.
- $this->backend->delete($cid);
- return false;
- }
- return $entry;
- }
- /**
- * {@inheritdoc}
- */
- public function getMultiple(&$cids)
- {
- $ret = array();
- $delete = array();
- if (!$this->allowPipeline) {
- $entries = array();
- foreach ($cids as $cid) {
- if ($entry = $this->backend->get($cid)) {
- $entries[$cid] = $entry;
- }
- }
- } else {
- $entries = $this->backend->getMultiple($cids);
- }
- list($flushPerm, $flushVolatile) = $this->getLastFlushTime();
- foreach ($cids as $key => $cid) {
- if (!empty($entries[$cid])) {
- $entry = $this->expandEntry($entries[$cid], $flushPerm, $flushVolatile);
- } else {
- $entry = null;
- }
- if (empty($entry)) {
- $delete[] = $cid;
- } else {
- $ret[$cid] = $entry;
- unset($cids[$key]);
- }
- }
- if (!empty($delete)) {
- if ($this->allowPipeline) {
- foreach ($delete as $id) {
- $this->backend->delete($id);
- }
- } else {
- $this->backend->deleteMultiple($delete);
- }
- }
- return $ret;
- }
- /**
- * {@inheritdoc}
- */
- public function set($cid, $data, $expire = CACHE_PERMANENT)
- {
- $hash = $this->createEntryHash($cid, $data, $expire);
- $maxTtl = $this->getMaxTtl();
- switch ($expire) {
- case CACHE_PERMANENT:
- $this->backend->set($cid, $hash, $maxTtl, false);
- break;
- case CACHE_TEMPORARY:
- $this->backend->set($cid, $hash, $maxTtl, true);
- break;
- default:
- $ttl = $expire - time();
- // Ensure $expire consistency
- if ($ttl <= 0) {
- // Entry has already expired, but we may have a stalled
- // older cache entry remaining there, ensure it wont
- // happen by doing a preventive delete
- $this->backend->delete($cid);
- } else {
- if ($maxTtl && $maxTtl < $ttl) {
- $ttl = $maxTtl;
- }
- $this->backend->set($cid, $hash, $ttl, false);
- }
- break;
- }
- }
- /**
- * {@inheritdoc}
- */
- public function clear($cid = null, $wildcard = false)
- {
- if (null === $cid && !$wildcard) {
- // Drupal asked for volatile entries flush, this will happen
- // during cron run, mostly
- $this->setLastFlushTime(false, true);
- if (!$this->isSharded && $this->allowTemporaryFlush) {
- $this->backend->flushVolatile();
- }
- } else if ($wildcard) {
- if (empty($cid)) {
- // This seems to be an error, just do nothing.
- return;
- }
- if ('*' === $cid) {
- // Use max() to ensure we invalidate both correctly
- $this->setLastFlushTime(true);
- if (!$this->isSharded) {
- $this->backend->flush();
- }
- } else {
- if (!$this->isSharded) {
- $this->backend->deleteByPrefix($cid);
- } else {
- // @todo This needs a map algorithm the same way memcache
- // module implemented it for invalidity by prefixes. This
- // is a very stupid fallback
- $this->setLastFlushTime(true);
- }
- }
- } else if (is_array($cid)) {
- $this->backend->deleteMultiple($cid);
- } else {
- $this->backend->delete($cid);
- }
- }
- public function isEmpty()
- {
- return false;
- }
- /**
- * From the given timestamp build an incremental safe time-based identifier.
- *
- * Due to potential accidental cache wipes, when a server goes down in the
- * cluster or when a server triggers its LRU algorithm wipe-out, keys that
- * matches flush or tags checksum might be dropped.
- *
- * Per default, each new inserted tag will trigger a checksum computation to
- * be stored in the Redis server as a timestamp. In order to ensure a checksum
- * validity a simple comparison between the tag checksum and the cache entry
- * checksum will tell us if the entry pre-dates the current checksum or not,
- * thus telling us its state. The main problem we experience is that Redis
- * is being so fast it is able to create and drop entries at same second,
- * sometime even the same micro second. The only safe way to avoid conflicts
- * is to checksum using an arbitrary computed number (a sequence).
- *
- * Drupal core does exactly this thus tags checksums are additions of each tag
- * individual checksum; each tag checksum is a independent arbitrary serial
- * that gets incremented starting with 0 (no invalidation done yet) to n (n
- * invalidations) which grows over time. This way the checksum computation
- * always rises and we have a sensible default that works in all cases.
- *
- * This model works as long as you can ensure consistency for the serial
- * storage over time. Nevertheless, as explained upper, in our case this
- * serial might be dropped at some point for various valid technical reasons:
- * if we start over to 0, we may accidentally compute a checksum which already
- * existed in the past and make invalid entries turn back to valid again.
- *
- * In order to prevent this behavior, using a timestamp as part of the serial
- * ensures that we won't experience this problem in a time range wider than a
- * single second, which is safe enough for us. But using timestamp creates a
- * new problem: Redis is so fast that we can set or delete hundreds of entries
- * easily during the same second: an entry created then invalidated the same
- * second will create false positives (entry is being considered as valid) -
- * note that depending on the check algorithm, false negative may also happen
- * the same way. Therefore we need to have an abitrary serial value to be
- * incremented in order to enforce our checks to be more strict.
- *
- * The solution to both the first (the need for a time based checksum in case
- * of checksum data being dropped) and the second (the need to have an
- * arbitrary predictible serial value to avoid false positives or negatives)
- * we are combining the two: every checksum will be built this way:
- *
- * UNIXTIMESTAMP.SERIAL
- *
- * For example:
- *
- * 1429789217.017
- *
- * will reprensent the 17th invalidation of the 1429789217 exact second which
- * happened while writing this documentation. The next tag being invalidated
- * the same second will then have this checksum:
- *
- * 1429789217.018
- *
- * And so on...
- *
- * In order to make it consitent with PHP string and float comparison we need
- * to set fixed precision over the decimal, and store as a string to avoid
- * possible float precision problems when comparing.
- *
- * This algorithm is not fully failsafe, but allows us to proceed to 1000
- * operations on the same checksum during the same second, which is a
- * sufficiently great value to reduce the conflict probability to almost
- * zero for most uses cases.
- *
- * @param int|string $timestamp
- * "TIMESTAMP[.INCREMENT]" string
- *
- * @return string
- * The next "TIMESTAMP.INCREMENT" string.
- */
- public function getNextIncrement($timestamp = null)
- {
- if (!$timestamp) {
- return time() . '.000';
- }
- if (false !== ($pos = strpos($timestamp, '.'))) {
- $inc = substr($timestamp, $pos + 1, 3);
- return ((int)$timestamp) . '.' . str_pad($inc + 1, 3, '0', STR_PAD_LEFT);
- }
- return $timestamp . '.000';
- }
- /**
- * Get valid checksum
- *
- * @param int|string $previous
- * "TIMESTAMP[.INCREMENT]" string
- *
- * @return string
- * The next "TIMESTAMP.INCREMENT" string.
- *
- * @see Redis_Cache::getNextIncrement()
- */
- public function getValidChecksum($previous = null)
- {
- if (time() === (int)$previous) {
- return $this->getNextIncrement($previous);
- } else {
- return $this->getNextIncrement();
- }
- }
- }
|