X-Git-Url: https://git.cyclocoop.org/?a=blobdiff_plain;ds=sidebyside;f=includes%2Flibs%2Fobjectcache%2FWANObjectCache.php;h=6ef2bce0b7ad69546d0ec0510c3dbbdf1010ab47;hb=220285ddd27b7f2971e9619940c48a8eee122cd0;hp=6c69b14fb521a7e5ec758ed26b701a3d7c19cee4;hpb=b2222eacf7050acabe7d0172e1822c67a2fdfa7e;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/objectcache/WANObjectCache.php b/includes/libs/objectcache/WANObjectCache.php index 6c69b14fb5..8bdafcfa4b 100644 --- a/includes/libs/objectcache/WANObjectCache.php +++ b/includes/libs/objectcache/WANObjectCache.php @@ -20,6 +20,10 @@ * @author Aaron Schulz */ +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + /** * Multi-datacenter aware caching interface * @@ -29,11 +33,14 @@ * This class is intended for caching data from primary stores. * If the get() method does not return a value, then the caller * should query the new value and backfill the cache using set(). + * When querying the store on cache miss, the closest DB replica + * should be used. Try to avoid heavyweight DB master or quorum reads. * When the source data changes, a purge method should be called. * Since purges are expensive, they should be avoided. One can do so if: * - a) The object cached is immutable; or * - b) Validity is checked against the source after get(); or * - c) Using a modest TTL is reasonably correct and performant + * * The simplest purge method is delete(). * * Instances of this class must be configured to point to a valid @@ -57,54 +64,62 @@ * @ingroup Cache * @since 1.26 */ -class WANObjectCache { +class WANObjectCache implements IExpiringStore, LoggerAwareInterface { /** @var BagOStuff The local datacenter cache */ protected $cache; + /** @var HashBagOStuff Script instance PHP cache */ + protected $procCache; /** @var string Cache pool name */ protected $pool; - /** @var EventRelayer */ + /** @var EventRelayer Bus that handles purge broadcasts */ protected $relayer; + /** @var LoggerInterface */ + protected $logger; - /** @var int */ + /** @var int ERR_* constant for the "last error" registry */ protected $lastRelayError = self::ERR_NONE; /** Max time expected to pass between delete() and DB commit finishing */ const MAX_COMMIT_DELAY = 3; - /** Max replication lag before applying TTL_LAGGED to set() */ - const MAX_REPLICA_LAG = 5; - /** Max time since snapshot transaction start to avoid no-op of set() */ - const MAX_SNAPSHOT_LAG = 5; + /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */ + const MAX_READ_LAG = 7; /** Seconds to tombstone keys on delete() */ - const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG + 1 + const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1 /** Seconds to keep dependency purge keys around */ - const CHECK_KEY_TTL = 31536000; // 1 year + const CHECK_KEY_TTL = self::TTL_YEAR; /** Seconds to keep lock keys around */ - const LOCK_TTL = 5; + const LOCK_TTL = 10; /** Default remaining TTL at which to consider pre-emptive regeneration */ const LOW_TTL = 30; /** Default time-since-expiry on a miss that makes a key "hot" */ const LOCK_TSE = 1; - /** Idiom for set()/getWithSetCallback() TTL */ - const TTL_NONE = 0; /** Idiom for getWithSetCallback() callbacks to avoid calling set() */ const TTL_UNCACHEABLE = -1; /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */ const TSE_NONE = -1; /** Max TTL to store keys when a data sourced is lagged */ const TTL_LAGGED = 30; + /** Idiom for delete() for "no hold-off" */ + const HOLDOFF_NONE = 0; + + /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */ + const TINY_NEGATIVE = -0.000001; /** Cache format version number */ const VERSION = 1; - /** Fields of value holder arrays */ const FLD_VERSION = 0; const FLD_VALUE = 1; const FLD_TTL = 2; const FLD_TIME = 3; + const FLD_FLAGS = 4; + const FLD_HOLDOFF = 5; + + /** @var integer Treat this value as expired-on-arrival */ + const FLG_STALE = 1; - /** Possible values for getLastError() */ const ERR_NONE = 0; // no error const ERR_NO_RESPONSE = 1; // no response const ERR_UNREACHABLE = 2; // can't connect @@ -117,20 +132,31 @@ class WANObjectCache { const PURGE_VAL_PREFIX = 'PURGED:'; + const MAX_PC_KEYS = 1000; // max keys to keep in process cache + /** * @param array $params * - cache : BagOStuff object * - pool : pool name * - relayer : EventRelayer object + * - logger : LoggerInterface object */ public function __construct( array $params ) { $this->cache = $params['cache']; $this->pool = $params['pool']; $this->relayer = $params['relayer']; + $this->procCache = new HashBagOStuff( array( 'maxKeys' => self::MAX_PC_KEYS ) ); + $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() ); + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; } /** - * @return WANObjectCache Cache that wraps EmptyBagOStuff + * Get an instance that wraps EmptyBagOStuff + * + * @return WANObjectCache */ public static function newEmpty() { return new self( array( @@ -143,11 +169,11 @@ class WANObjectCache { /** * Fetch the value of a key from cache * - * If passed in, $curTTL is set to the remaining TTL (current time left): - * - a) INF; if the key exists, has no TTL, and is not expired by $checkKeys - * - b) float (>=0); if the key exists, has a TTL, and is not expired by $checkKeys - * - c) float (<0); if the key is tombstoned or existing but expired by $checkKeys - * - d) null; if the key does not exist and is not tombstoned + * If supplied, $curTTL is set to the remaining TTL (current time left): + * - a) INF; if $key exists, has no TTL, and is not expired by $checkKeys + * - b) float (>=0); if $key exists, has a TTL, and is not expired by $checkKeys + * - c) float (<0); if $key is tombstoned, stale, or existing but expired by $checkKeys + * - d) null; if $key does not exist and is not tombstoned * * If a key is tombstoned, $curTTL will reflect the time since delete(). * @@ -171,7 +197,7 @@ class WANObjectCache { * However, pre-snapshot values might still be seen if an update was made * in a remote datacenter but the purge from delete() didn't relay yet. * - * Consider using getWithSetCallback() instead of get()/set() cycles. + * Consider using getWithSetCallback() instead of get() and set() cycles. * That method has cache slam avoiding features for hot/expensive keys. * * @param string $key Cache key @@ -194,7 +220,8 @@ class WANObjectCache { * * @param array $keys List of cache keys * @param array $curTTLs Map of (key => approximate TTL left) for existing keys [returned] - * @param array $checkKeys List of "check" keys + * @param array $checkKeys List of check keys to apply to all $keys. May also apply "check" + * keys to specific cache keys only by using cache keys as keys in the $checkKeys array. * @return array Map of (key => value) for keys that exist */ final public function getMulti( @@ -205,26 +232,34 @@ class WANObjectCache { $vPrefixLen = strlen( self::VALUE_KEY_PREFIX ); $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX ); - $checkKeys = self::prefixCacheKeys( $checkKeys, self::TIME_KEY_PREFIX ); + + $checkKeysForAll = array(); + $checkKeysByKey = array(); + $checkKeysFlat = array(); + foreach ( $checkKeys as $i => $keys ) { + $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX ); + $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed ); + // Is this check keys for a specific cache key, or for all keys being fetched? + if ( is_int( $i ) ) { + $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed ); + } else { + $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] ) + ? array_merge( $checkKeysByKey[$i], $prefixed ) + : $prefixed; + } + } // Fetch all of the raw values - $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeys ) ); + $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) ); + // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic) $now = microtime( true ); - // Get/initialize the timestamp of all the "check" keys - $checkKeyTimes = array(); - foreach ( $checkKeys as $checkKey ) { - $timestamp = isset( $wrappedValues[$checkKey] ) - ? self::parsePurgeValue( $wrappedValues[$checkKey] ) - : false; - if ( !is_float( $timestamp ) ) { - // Key is not set or invalid; regenerate - $this->cache->add( $checkKey, - self::PURGE_VAL_PREFIX . $now, self::CHECK_KEY_TTL ); - $timestamp = $now; - } - - $checkKeyTimes[] = $timestamp; + // Collect timestamps from all "check" keys + $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now ); + $purgeValuesByKey = array(); + foreach ( $checkKeysByKey as $cacheKey => $checks ) { + $purgeValuesByKey[$cacheKey] = + $this->processCheckKeys( $checks, $wrappedValues, $now ); } // Get the main cache value for each key and validate them @@ -238,16 +273,23 @@ class WANObjectCache { list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now ); if ( $value !== false ) { $result[$key] = $value; - foreach ( $checkKeyTimes as $checkKeyTime ) { - // Force dependant keys to be invalid for a while after purging - // to reduce race conditions involving stale data getting cached - $safeTimestamp = $checkKeyTime + self::HOLDOFF_TTL; + + // Force dependant keys to be invalid for a while after purging + // to reduce race conditions involving stale data getting cached + $purgeValues = $purgeValuesForAll; + if ( isset( $purgeValuesByKey[$key] ) ) { + $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] ); + } + foreach ( $purgeValues as $purge ) { + $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF]; if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) { - $curTTL = min( $curTTL, $checkKeyTime - $now ); + // How long ago this value was expired by *this* check key + $ago = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE ); + // How long ago this value was expired by *any* known check key + $curTTL = min( $curTTL, $ago ); } } } - $curTTLs[$key] = $curTTL; } @@ -255,7 +297,33 @@ class WANObjectCache { } /** - * Set the value of a key from cache + * @since 1.27 + * @param array $timeKeys List of prefixed time check keys + * @param array $wrappedValues + * @param float $now + * @return array List of purge value arrays + */ + private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) { + $purgeValues = array(); + foreach ( $timeKeys as $timeKey ) { + $purge = isset( $wrappedValues[$timeKey] ) + ? self::parsePurgeValue( $wrappedValues[$timeKey] ) + : false; + if ( $purge === false ) { + // Key is not set or invalid; regenerate + $this->cache->add( $timeKey, + $this->makePurgeValue( $now, self::HOLDOFF_TTL ), + self::CHECK_KEY_TTL + ); + $purge = array( self::FLD_TIME => $now, self::FLD_HOLDOFF => self::HOLDOFF_TTL ); + } + $purgeValues[] = $purge; + } + return $purgeValues; + } + + /** + * Set the value of a key in cache * * Simply calling this method when source data changes is not valid because * the changes do not replicate to the other WAN sites. In that case, delete() @@ -269,7 +337,7 @@ class WANObjectCache { * - d) T1 reads the row and calls set() due to a cache miss * - e) Stale value is stuck in cache * - * Setting 'lag' helps avoids keys getting stuck in long-term stale states. + * Setting 'lag' and 'since' help avoids keys getting stuck in stale states. * * Example usage: * @code @@ -277,27 +345,31 @@ class WANObjectCache { * $setOpts = Database::getCacheSetOptions( $dbr ); * // Fetch the row from the DB * $row = $dbr->selectRow( ... ); - * $key = wfMemcKey( 'building', $buildingId ); - * $cache->set( $key, $row, 86400, $setOpts ); + * $key = $cache->makeKey( 'building', $buildingId ); + * $cache->set( $key, $row, $cache::TTL_DAY, $setOpts ); * @endcode * * @param string $key Cache key * @param mixed $value - * @param integer $ttl Seconds to live [0=forever] + * @param integer $ttl Seconds to live. Special values are: + * - WANObjectCache::TTL_INDEFINITE: Cache forever * @param array $opts Options map: * - lag : Seconds of slave lag. Typically, this is either the slave lag * before the data was read or, if applicable, the slave lag before * the snapshot-isolated transaction the data was read from started. - * [Default: 0 seconds] + * Default: 0 seconds * - since : UNIX timestamp of the data in $value. Typically, this is either * the current time the data was read or (if applicable) the time when * the snapshot-isolated transaction the data was read from started. - * [Default: 0 seconds] - * - lockTSE : if excessive possible snapshot lag is detected, - * then stash the value into a temporary location - * with this TTL. This is only useful if the reads - * use getWithSetCallback() with "lockTSE" set. - * [Default: WANObjectCache::TSE_NONE] + * Default: 0 seconds + * - pending : Whether this data is possibly from an uncommitted write transaction. + * Generally, other threads should not see values from the future and + * they certainly should not see ones that ended up getting rolled back. + * Default: false + * - lockTSE : if excessive replication/snapshot lag is detected, then store the value + * with this TTL and flag it as stale. This is only useful if the reads for + * this key use getWithSetCallback() with "lockTSE" set. + * Default: WANObjectCache::TSE_NONE * @return bool Success */ final public function set( $key, $value, $ttl = 0, array $opts = array() ) { @@ -305,21 +377,39 @@ class WANObjectCache { $age = isset( $opts['since'] ) ? max( 0, microtime( true ) - $opts['since'] ) : 0; $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0; - if ( $lag > self::MAX_REPLICA_LAG ) { - // Too much lag detected; lower TTL so it converges faster - $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED; + // Do not cache potentially uncommitted data as it might get rolled back + if ( !empty( $opts['pending'] ) ) { + $this->logger->info( "Rejected set() for $key due to pending writes." ); + + return true; // no-op the write for being unsafe } - if ( $age > self::MAX_SNAPSHOT_LAG ) { + $wrapExtra = array(); // additional wrapped value fields + // Check if there's a risk of writing stale data after the purge tombstone expired + if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) { + // Case A: read lag with "lockTSE"; save but record value as stale if ( $lockTSE >= 0 ) { - $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds - $this->cache->set( self::STASH_KEY_PREFIX . $key, $value, $tempTTL ); - } + $ttl = max( 1, (int)$lockTSE ); // set() expects seconds + $wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale + // Case B: any long-running transaction; ignore this set() + } elseif ( $age > self::MAX_READ_LAG ) { + $this->logger->warning( "Rejected set() for $key due to snapshot lag." ); + + return true; // no-op the write for being unsafe + // Case C: high replication lag; lower TTL instead of ignoring all set()s + } elseif ( $lag === false || $lag > self::MAX_READ_LAG ) { + $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED; + $this->logger->warning( "Lowered set() TTL for $key due to replication lag." ); + // Case D: medium length request with medium replication lag; ignore this set() + } else { + $this->logger->warning( "Rejected set() for $key due to high read lag." ); - return true; // no-op the write for being unsafe + return true; // no-op the write for being unsafe + } } - $wrapped = $this->wrap( $value, $ttl ); + // Wrap that value with time/TTL/version metadata + $wrapped = $this->wrap( $value, $ttl ) + $wrapExtra; $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) { return ( is_string( $cWrapped ) ) @@ -344,10 +434,12 @@ class WANObjectCache { * This is implemented by storing a special "tombstone" value at the cache * key that this class recognizes; get() calls will return false for the key * and any set() calls will refuse to replace tombstone values at the key. - * For this to always avoid writing stale values, the following must hold: + * For this to always avoid stale value writes, the following must hold: * - a) Replication lag is bounded to being less than HOLDOFF_TTL; or * - b) If lag is higher, the DB will have gone into read-only mode already * + * Note that set() can also be lag-aware and lower the TTL if it's high. + * * When using potentially long-running ACID transactions, a good pattern is * to use a pre-commit hook to issue the delete. This means that immediately * after commit, callers will see the tombstone in cache in the local datacenter @@ -364,7 +456,7 @@ class WANObjectCache { * ... ... * // Update the row in the DB * $dbw->update( ... ); - * $key = wfMemcKey( 'homes', $homeId ); + * $key = $cache->makeKey( 'homes', $homeId ); * // Purge the corresponding cache entry just before committing * $dbw->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) { * $cache->delete( $key ); @@ -373,23 +465,37 @@ class WANObjectCache { * $dbw->commit(); // end of request * @endcode * - * If called twice on the same key, then the last hold-off TTL takes - * precedence. For idempotence, the $ttl should not vary for different - * delete() calls on the same key. Also note that lowering $ttl reduces - * the effective range of the 'lockTSE' parameter to getWithSetCallback(). + * The $ttl parameter can be used when purging values that have not actually changed + * recently. For example, a cleanup script to purge cache entries does not really need + * a hold-off period, so it can use HOLDOFF_NONE. Likewise for user-requested purge. + * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback(). + * + * If called twice on the same key, then the last hold-off TTL takes precedence. For + * idempotence, the $ttl should not vary for different delete() calls on the same key. * * @param string $key Cache key - * @param integer $ttl How long to block writes to the key [seconds] + * @param integer $ttl Tombstone TTL; Default: WANObjectCache::HOLDOFF_TTL * @return bool True if the item was purged or not found, false on failure */ final public function delete( $key, $ttl = self::HOLDOFF_TTL ) { $key = self::VALUE_KEY_PREFIX . $key; - // Avoid indefinite key salting for sanity - $ttl = max( $ttl, 1 ); - // Update the local datacenter immediately - $ok = $this->cache->set( $key, self::PURGE_VAL_PREFIX . microtime( true ), $ttl ); - // Publish the purge to all datacenters - return $this->relayPurge( $key, $ttl ) && $ok; + + if ( $ttl <= 0 ) { + // Update the local datacenter immediately + $ok = $this->cache->delete( $key ); + // Publish the purge to all datacenters + $ok = $this->relayDelete( $key ) && $ok; + } else { + // Update the local datacenter immediately + $ok = $this->cache->set( $key, + $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ), + $ttl + ); + // Publish the purge to all datacenters + $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok; + } + + return $ok; } /** @@ -406,20 +512,25 @@ class WANObjectCache { * if the key was evicted from cache, such calculations may show the * time since expiry as ~0 seconds. * - * Note that "check" keys won't collide with other regular keys + * Note that "check" keys won't collide with other regular keys. * * @param string $key - * @return float UNIX timestamp of the key + * @return float UNIX timestamp of the check key */ final public function getCheckKeyTime( $key ) { $key = self::TIME_KEY_PREFIX . $key; - $time = self::parsePurgeValue( $this->cache->get( $key ) ); - if ( $time === false ) { + $purge = self::parsePurgeValue( $this->cache->get( $key ) ); + if ( $purge !== false ) { + $time = $purge[self::FLD_TIME]; + } else { // Casting assures identical floats for the next getCheckKeyTime() calls - $time = (string)microtime( true ); - $this->cache->add( $key, self::PURGE_VAL_PREFIX . $time, self::CHECK_KEY_TTL ); - $time = (float)$time; + $now = (string)microtime( true ); + $this->cache->add( $key, + $this->makePurgeValue( $now, self::HOLDOFF_TTL ), + self::CHECK_KEY_TTL + ); + $time = (float)$now; } return $time; @@ -434,38 +545,44 @@ class WANObjectCache { * keys, the relevant "check" keys must be supplied for this to work. * * The "check" key essentially represents a last-modified field. - * It is set in the future a few seconds when this is called, to - * avoid race conditions where dependent keys get updated with a - * stale value (e.g. from a DB slave). + * When touched, keys using it via get(), getMulti(), or getWithSetCallback() + * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future + * by those methods to avoid race conditions where dependent keys get updated + * with stale values (e.g. from a DB slave). * - * This is typically useful for keys with static names or some cases + * This is typically useful for keys with hardcoded names or in some cases * dynamically generated names where a low number of combinations exist. * When a few important keys get a large number of hits, a high cache - * time is usually desired as well as lockTSE logic. The resetCheckKey() + * time is usually desired as well as "lockTSE" logic. The resetCheckKey() * method is less appropriate in such cases since the "time since expiry" * cannot be inferred. * - * Note that "check" keys won't collide with other regular keys + * Note that "check" keys won't collide with other regular keys. * * @see WANObjectCache::get() + * @see WANObjectCache::getWithSetCallback() + * @see WANObjectCache::resetCheckKey() * * @param string $key Cache key + * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant * @return bool True if the item was purged or not found, false on failure */ - final public function touchCheckKey( $key ) { + final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) { $key = self::TIME_KEY_PREFIX . $key; // Update the local datacenter immediately $ok = $this->cache->set( $key, - self::PURGE_VAL_PREFIX . microtime( true ), self::CHECK_KEY_TTL ); + $this->makePurgeValue( microtime( true ), $holdoff ), + self::CHECK_KEY_TTL + ); // Publish the purge to all datacenters - return $this->relayPurge( $key, self::CHECK_KEY_TTL ) && $ok; + return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok; } /** * Delete a "check" key from all datacenters, invalidating keys that use it * - * This is similar to touchCheckKey() in that keys using it via - * getWithSetCallback() will be invalidated. The differences are: + * This is similar to touchCheckKey() in that keys using it via get(), getMulti(), + * or getWithSetCallback() will be invalidated. The differences are: * - a) The timestamp will be deleted from all caches and lazily * re-initialized when accessed (rather than set everywhere) * - b) Thus, dependent keys will be known to be invalid, but not @@ -479,10 +596,11 @@ class WANObjectCache { * This is typically useful for keys with dynamically generated names * where a high number of combinations exist. * - * Note that "check" keys won't collide with other regular keys + * Note that "check" keys won't collide with other regular keys. * - * @see WANObjectCache::touchCheckKey() * @see WANObjectCache::get() + * @see WANObjectCache::getWithSetCallback() + * @see WANObjectCache::touchCheckKey() * * @param string $key Cache key * @return bool True if the item was purged or not found, false on failure @@ -499,24 +617,21 @@ class WANObjectCache { * Method to fetch/regenerate cache keys * * On cache miss, the key will be set to the callback result via set() - * unless the callback returns false. The arguments supplied to it are: - * (current value or false, &$ttl, &$setOpts) - * The callback function returns the new value given the current - * value (false if not present). Preemptive re-caching and $checkKeys - * can result in a non-false current value. The TTL of the new value - * can be set dynamically by altering $ttl in the callback (by reference). - * The $setOpts array can be altered and is given to set() when called; - * it is recommended to set the 'since' field to avoid race conditions. - * Setting 'lag' helps avoids keys getting stuck in long-term stale states. - * - * Usually, callbacks ignore the current value, but it can be used - * to maintain "most recent X" values that come from time or sequence - * based source data, provided that the "as of" id/time is tracked. - * - * Usage of $checkKeys is similar to get() and getMulti(). However, - * rather than the caller having to inspect a "current time left" - * variable (e.g. $curTTL, $curTTLs), a cache regeneration will be - * triggered using the callback. + * (unless the callback returns false) and that result will be returned. + * The arguments supplied to the callback are: + * - $oldValue : current cache value or false if not present + * - &$ttl : a reference to the TTL which can be altered + * - &$setOpts : a reference to options for set() which can be altered + * + * It is strongly recommended to set the 'lag' and 'since' fields to avoid race conditions + * that can cause stale values to get stuck at keys. Usually, callbacks ignore the current + * value, but it can be used to maintain "most recent X" values that come from time or + * sequence based source data, provided that the "as of" id/time is tracked. Note that + * preemptive regeneration and $checkKeys can result in a non-false current value. + * + * Usage of $checkKeys is similar to get() and getMulti(). However, rather than the caller + * having to inspect a "current time left" variable (e.g. $curTTL, $curTTLs), a cache + * regeneration will automatically be triggered using the callback. * * The simplest way to avoid stampedes for hot keys is to use * the 'lockTSE' option in $opts. If cache purges are needed, also: @@ -527,9 +642,9 @@ class WANObjectCache { * @code * $catInfo = $cache->getWithSetCallback( * // Key to store the cached value under - * wfMemcKey( 'cat-attributes', $catId ), - * // Time-to-live (seconds) - * 60, + * $cache->makeKey( 'cat-attributes', $catId ), + * // Time-to-live (in seconds) + * $cache::TTL_MINUTE, * // Function that derives the new key value * function ( $oldValue, &$ttl, array &$setOpts ) { * $dbr = wfGetDB( DB_SLAVE ); @@ -545,9 +660,9 @@ class WANObjectCache { * @code * $catConfig = $cache->getWithSetCallback( * // Key to store the cached value under - * wfMemcKey( 'site-cat-config' ), - * // Time-to-live (seconds) - * 86400, + * $cache->makeKey( 'site-cat-config' ), + * // Time-to-live (in seconds) + * $cache::TTL_DAY, * // Function that derives the new key value * function ( $oldValue, &$ttl, array &$setOpts ) { * $dbr = wfGetDB( DB_SLAVE ); @@ -556,9 +671,9 @@ class WANObjectCache { * * return CatConfig::newFromRow( $dbr->selectRow( ... ) ); * }, - * // Calling touchCheckKey() on this key invalidates the cache * array( - * 'checkKeys' => array( wfMemcKey( 'site-cat-config' ) ), + * // Calling touchCheckKey() on this key invalidates the cache + * 'checkKeys' => array( $cache->makeKey( 'site-cat-config' ) ), * // Try to only let one datacenter thread manage cache updates at a time * 'lockTSE' => 30 * ) @@ -569,9 +684,9 @@ class WANObjectCache { * @code * $catState = $cache->getWithSetCallback( * // Key to store the cached value under - * wfMemcKey( 'cat-state', $cat->getId() ), + * $cache->makeKey( 'cat-state', $cat->getId() ), * // Time-to-live (seconds) - * 900, + * $cache::TTL_HOUR, * // Function that derives the new key value * function ( $oldValue, &$ttl, array &$setOpts ) { * // Determine new value from the DB @@ -581,13 +696,13 @@ class WANObjectCache { * * return CatState::newFromResults( $dbr->select( ... ) ); * }, - * // The "check" keys that represent things the value depends on; - * // Calling touchCheckKey() on any of them invalidates the cache * array( + * // The "check" keys that represent things the value depends on; + * // Calling touchCheckKey() on any of them invalidates the cache * 'checkKeys' => array( - * wfMemcKey( 'sustenance-bowls', $cat->getRoomId() ), - * wfMemcKey( 'people-present', $cat->getHouseId() ), - * wfMemcKey( 'cat-laws', $cat->getCityId() ), + * $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ), + * $cache->makeKey( 'people-present', $cat->getHouseId() ), + * $cache->makeKey( 'cat-laws', $cat->getCityId() ), * ) * ) * ); @@ -597,8 +712,8 @@ class WANObjectCache { * @code * $lastCatActions = $cache->getWithSetCallback( * // Key to store the cached value under - * wfMemcKey( 'cat-last-actions', 100 ), - * // Time-to-live (seconds) + * $cache->makeKey( 'cat-last-actions', 100 ), + * // Time-to-live (in seconds) * 10, * // Function that derives the new key value * function ( $oldValue, &$ttl, array &$setOpts ) { @@ -624,11 +739,12 @@ class WANObjectCache { * * @param string $key Cache key * @param integer $ttl Seconds to live for key updates. Special values are: - * - WANObjectCache::TTL_NONE : Cache forever + * - WANObjectCache::TTL_INDEFINITE: Cache forever * - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all * @param callable $callback Value generation function * @param array $opts Options map: - * - checkKeys: List of "check" keys. + * - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either + * touchCheckKey() or resetCheckKey() is called on any of these keys. * - lowTTL: Consider pre-emptive updates when the current TTL (sec) of the key is less than * this. It becomes more likely over time, becoming a certainty once the key is expired. * Default: WANObjectCache::LOW_TTL seconds. @@ -638,28 +754,49 @@ class WANObjectCache { * expiration is low, the assumption is that the key is hot and that a stampede is worth * avoiding. Setting this above WANObjectCache::HOLDOFF_TTL makes no difference. The * higher this is set, the higher the worst-case staleness can be. - * Use WANObjectCache::TSE_NONE to disable this logic. Default: WANObjectCache::TSE_NONE. + * Use WANObjectCache::TSE_NONE to disable this logic. + * Default: WANObjectCache::TSE_NONE. + * - pcTTL : process cache the value in this PHP instance with this TTL. This avoids + * network I/O when a key is read several times. This will not cache if the callback + * returns false however. Note that any purges will not be seen while process cached; + * since the callback should use slave DBs and they may be lagged or have snapshot + * isolation anyway, this should not typically matter. + * Default: WANObjectCache::TTL_UNCACHEABLE. * @return mixed Value to use for the key */ - final public function getWithSetCallback( - $key, $ttl, $callback, array $opts = array(), $oldOpts = array() - ) { - // Back-compat with 1.26: Swap $ttl and $callback - if ( is_int( $callback ) ) { - $temp = $ttl; - $ttl = $callback; - $callback = $temp; - } - // Back-compat with 1.26: $checkKeys as separate parameter - if ( $oldOpts || ( is_array( $opts ) && isset( $opts[0] ) ) ) { - $checkKeys = $opts; - $opts = $oldOpts; - } else { - $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : array(); + final public function getWithSetCallback( $key, $ttl, $callback, array $opts = array() ) { + $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE; + + // Try the process cache if enabled + $value = ( $pcTTL >= 0 ) ? $this->procCache->get( $key ) : false; + + if ( $value === false ) { + // Fetch the value over the network + $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts ); + // Update the process cache if enabled + if ( $pcTTL >= 0 && $value !== false ) { + $this->procCache->set( $key, $value, $pcTTL ); + } } + return $value; + } + + /** + * Do the actual I/O for getWithSetCallback() when needed + * + * @see WANObjectCache::getWithSetCallback() + * + * @param string $key + * @param integer $ttl + * @param callback $callback + * @param array $opts + * @return mixed + */ + protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts ) { $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl ); $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE; + $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : array(); // Get the current key value $curTTL = null; @@ -728,6 +865,26 @@ class WANObjectCache { return $value; } + /** + * @see BagOStuff::makeKey() + * @param string ... Key component + * @return string + * @since 1.27 + */ + public function makeKey() { + return call_user_func_array( array( $this->cache, __FUNCTION__ ), func_get_args() ); + } + + /** + * @see BagOStuff::makeGlobalKey() + * @param string ... Key component + * @return string + * @since 1.27 + */ + public function makeGlobalKey() { + return call_user_func_array( array( $this->cache, __FUNCTION__ ), func_get_args() ); + } + /** * Get the "last error" registered; clearLastError() should be called manually * @return int ERR_* constant for the "last error" registry @@ -766,17 +923,18 @@ class WANObjectCache { /** * Do the actual async bus purge of a key * - * This must set the key to "PURGED:" + * This must set the key to "PURGED::" * * @param string $key Cache key * @param integer $ttl How long to keep the tombstone [seconds] + * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key * @return bool Success */ - protected function relayPurge( $key, $ttl ) { + protected function relayPurge( $key, $ttl, $holdoff ) { $event = $this->cache->modifySimpleRelayEvent( array( 'cmd' => 'set', 'key' => $key, - 'val' => 'PURGED:$UNIXTIME$', + 'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff, 'ttl' => max( $ttl, 1 ), 'sbt' => true, // substitute $UNIXTIME$ with actual microtime ) ); @@ -838,7 +996,7 @@ class WANObjectCache { * * @param mixed $value * @param integer $ttl [0=forever] - * @return string + * @return array */ protected function wrap( $value, $ttl ) { return array( @@ -858,10 +1016,10 @@ class WANObjectCache { */ protected function unwrap( $wrapped, $now ) { // Check if the value is a tombstone - $purgeTimestamp = self::parsePurgeValue( $wrapped ); - if ( is_float( $purgeTimestamp ) ) { + $purge = self::parsePurgeValue( $wrapped ); + if ( $purge !== false ) { // Purged values should always have a negative current $ttl - $curTTL = min( -0.000001, $purgeTimestamp - $now ); + $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE ); return array( false, $curTTL ); } @@ -872,7 +1030,12 @@ class WANObjectCache { return array( false, null ); } - if ( $wrapped[self::FLD_TTL] > 0 ) { + $flags = isset( $wrapped[self::FLD_FLAGS] ) ? $wrapped[self::FLD_FLAGS] : 0; + if ( ( $flags & self::FLG_STALE ) == self::FLG_STALE ) { + // Treat as expired, with the cache time as the expiration + $age = $now - $wrapped[self::FLD_TIME]; + $curTTL = min( -$age, self::TINY_NEGATIVE ); + } elseif ( $wrapped[self::FLD_TTL] > 0 ) { // Get the approximate time left on the key $age = $now - $wrapped[self::FLD_TIME]; $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 ); @@ -899,17 +1062,36 @@ class WANObjectCache { } /** - * @param string $value String like "PURGED:" - * @return float|bool UNIX timestamp or false on failure + * @param string $value Wrapped value like "PURGED::" + * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer), + * or false if value isn't a valid purge value */ protected static function parsePurgeValue( $value ) { - $m = array(); - if ( is_string( $value ) && - preg_match( '/^' . self::PURGE_VAL_PREFIX . '([^:]+)$/', $value, $m ) + if ( !is_string( $value ) ) { + return false; + } + $segments = explode( ':', $value, 3 ); + if ( !isset( $segments[0] ) || !isset( $segments[1] ) + || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX ) { - return (float)$m[1]; - } else { return false; } + if ( !isset( $segments[2] ) ) { + // Back-compat with old purge values without holdoff + $segments[2] = self::HOLDOFF_TTL; + } + return array( + self::FLD_TIME => (float)$segments[1], + self::FLD_HOLDOFF => (int)$segments[2], + ); + } + + /** + * @param float $timestamp + * @param int $holdoff In seconds + * @return string Wrapped purge value + */ + protected function makePurgeValue( $timestamp, $holdoff ) { + return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff; } }