* @author Aaron Schulz
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
/**
* Multi-datacenter aware caching interface
*
* 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
* @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
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(
/**
* 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().
*
* 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
*
* @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(
$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
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;
}
}
/**
- * 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()
* - 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
* $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() ) {
$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 ) )
* 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
* ... <execute some stuff> ...
* // 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 );
* $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;
}
/**
* 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;
* 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
* 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
* 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:
* @code
* $catInfo = $cache->getWithSetCallback(
* // Key to store the cached value under
- * wfMemcKey( 'cat-attributes', $catId ),
+ * $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 );
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return $dbr->selectRow( ... );
- * },
- * // Time-to-live (seconds)
- * 60
+ * }
* );
* @endcode
*
* @code
* $catConfig = $cache->getWithSetCallback(
* // Key to store the cached value under
- * wfMemcKey( 'site-cat-config' ),
+ * $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 );
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatConfig::newFromRow( $dbr->selectRow( ... ) );
- * },
- * // Time-to-live (seconds)
- * 86400,
- * // Calling touchCheckKey() on this key invalidates the cache
- * wfMemcKey( 'site-cat-config' ),
- * // Try to only let one datacenter thread manage cache updates at a time
- * array( 'lockTSE' => 30 )
+ * },
+ * array(
+ * // 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
+ * )
* );
* @endcode
*
* @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)
+ * $cache::TTL_HOUR,
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
* // Determine new value from the DB
* $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatState::newFromResults( $dbr->select( ... ) );
- * },
- * // Time-to-live (seconds)
- * 900,
- * // The "check" keys that represent things the value depends on;
- * // Calling touchCheckKey() on any of them invalidates the cache
- * array(
- * wfMemcKey( 'sustenance-bowls', $cat->getRoomId() ),
- * wfMemcKey( 'people-present', $cat->getHouseId() ),
- * wfMemcKey( 'cat-laws', $cat->getCityId() ),
+ * },
+ * array(
+ * // The "check" keys that represent things the value depends on;
+ * // Calling touchCheckKey() on any of them invalidates the cache
+ * 'checkKeys' => array(
+ * $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ),
+ * $cache->makeKey( 'people-present', $cat->getHouseId() ),
+ * $cache->makeKey( 'cat-laws', $cat->getCityId() ),
+ * )
* )
* );
* @endcode
* @code
* $lastCatActions = $cache->getWithSetCallback(
* // Key to store the cached value under
- * wfMemcKey( 'cat-last-actions', 100 ),
+ * $cache->makeKey( 'cat-last-actions', 100 ),
+ * // Time-to-live (in seconds)
+ * 10,
* // Function that derives the new key value
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Merge them and get the new "last 100" rows
* return array_slice( array_merge( $new, $list ), 0, 100 );
* },
- * // Time-to-live (seconds)
- * 10,
- * // No "check" keys
- * array(),
* // Try to only let one datacenter thread manage cache updates at a time
* array( 'lockTSE' => 30 )
* );
*
* @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.
* 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;
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
/**
* Do the actual async bus purge of a key
*
- * This must set the key to "PURGED:<UNIX timestamp>"
+ * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
*
* @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
) );
*
* @param mixed $value
* @param integer $ttl [0=forever]
- * @return string
+ * @return array
*/
protected function wrap( $value, $ttl ) {
return array(
*/
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 );
}
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 );
}
/**
- * @param string $value String like "PURGED:<timestamp>"
- * @return float|bool UNIX timestamp or false on failure
+ * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
+ * @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;
}
}