/** @var int Key fetched */
private $warmupKeyMisses = 0;
+ /** @var float|null */
+ private $wallClockOverride;
+
/** Max time expected to pass between delete() and DB commit finishing */
const MAX_COMMIT_DELAY = 3;
/** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
*/
public function __construct( array $params ) {
$this->cache = $params['cache'];
- $this->purgeChannel = isset( $params['channels']['purge'] )
- ? $params['channels']['purge']
- : self::DEFAULT_PURGE_CHANNEL;
- $this->purgeRelayer = isset( $params['relayers']['purge'] )
- ? $params['relayers']['purge']
- : new EventRelayerNull( [] );
- $this->region = isset( $params['region'] ) ? $params['region'] : 'main';
- $this->cluster = isset( $params['cluster'] ) ? $params['cluster'] : 'wan-main';
+ $this->purgeChannel = $params['channels']['purge'] ?? self::DEFAULT_PURGE_CHANNEL;
+ $this->purgeRelayer = $params['relayers']['purge'] ?? new EventRelayerNull( [] );
+ $this->region = $params['region'] ?? 'main';
+ $this->cluster = $params['cluster'] ?? 'wan-main';
$this->mcrouterAware = !empty( $params['mcrouterAware'] );
- $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
- $this->stats = isset( $params['stats'] ) ? $params['stats'] : new NullStatsdDataFactory();
- $this->asyncHandler = isset( $params['asyncHandler'] ) ? $params['asyncHandler'] : null;
+ $this->setLogger( $params['logger'] ?? new NullLogger() );
+ $this->stats = $params['stats'] ?? new NullStatsdDataFactory();
+ $this->asyncHandler = $params['asyncHandler'] ?? null;
}
/**
* Fetch the value of a key from cache
*
* 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
+ * - a) INF; if $key exists, has no TTL, and is not invalidated by $checkKeys
+ * - b) float (>=0); if $key exists, has a TTL, and is not invalidated by $checkKeys
+ * - c) float (<0); if $key is tombstoned, stale, or existing but invalidated 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().
$curTTLs = [];
$asOfs = [];
$values = $this->getMulti( [ $key ], $curTTLs, $checkKeys, $asOfs );
- $curTTL = isset( $curTTLs[$key] ) ? $curTTLs[$key] : null;
- $asOf = isset( $asOfs[$key] ) ? $asOfs[$key] : null;
+ $curTTL = $curTTLs[$key] ?? null;
+ $asOf = $asOfs[$key] ?? null;
- return isset( $values[$key] ) ? $values[$key] : false;
+ return $values[$key] ?? false;
}
/**
list( $value, $curTTL ) = $this->unwrap( $wrappedValues[$vKey], $now );
if ( $value !== false ) {
$result[$key] = $value;
-
- // Force dependant keys to be invalid for a while after purging
+ // Force dependent keys to be seen as stale for a while after purging
// to reduce race conditions involving stale data getting cached
$purgeValues = $purgeValuesForAll;
if ( isset( $purgeValuesByKey[$key] ) ) {
foreach ( $purgeValues as $purge ) {
$safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
- // How long ago this value was expired by *this* check key
+ // How long ago this value was invalidated 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
+ // How long ago this value was invalidated by *any* known check key
$curTTL = min( $curTTL, $ago );
}
}
? $this->parsePurgeValue( $wrappedValues[$timeKey] )
: false;
if ( $purge === false ) {
- // Key is not set or invalid; regenerate
+ // Key is not set or malformed; regenerate
$newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
$this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
$purge = $this->parsePurgeValue( $newVal );
*/
final public function set( $key, $value, $ttl = 0, array $opts = [] ) {
$now = $this->getCurrentTime();
- $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
- $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE;
+ $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
+ $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
$age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
- $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
+ $lag = $opts['lag'] ?? 0;
// Do not cache potentially uncommitted data as it might get rolled back
if ( !empty( $opts['pending'] ) ) {
* or getWithSetCallback() will be invalidated. The differences are:
* - a) The "check" key 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
+ * - b) Thus, dependent keys will be known to be stale, but not
* for how long (they are treated as "just" purged), which
* effects any lockTSE logic in getWithSetCallback()
* - c) Since "check" keys are initialized only on the server the key hashes
* - WANObjectCache::TTL_UNCACHEABLE: Do not cache (if the key exists, it is not deleted)
* @param callable $callback Value generation function
* @param array $opts Options map:
- * - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either
+ * - checkKeys: List of "check" keys. The key at $key will be seen as stale when either
* touchCheckKey() or resetCheckKey() is called on any of the keys in this list. This
* is useful if thousands or millions of keys depend on the same entity. The entity can
* simply have its "check" key updated whenever the entity is modified.
* Default: [].
- * - graceTTL: Consider reusing expired values instead of refreshing them if they expired
- * less than this many seconds ago. The odds of a refresh becomes more likely over time,
+ * - graceTTL: If the key is invalidated (by "checkKeys") less than this many seconds ago,
+ * consider reusing the stale value. The odds of a refresh becomes more likely over time,
* becoming certain once the grace period is reached. This can reduce traffic spikes
- * when millions of keys are compared to the same "check" key and touchCheckKey()
- * or resetCheckKey() is called on that "check" key.
+ * when millions of keys are compared to the same "check" key and touchCheckKey() or
+ * resetCheckKey() is called on that "check" key. This option is not useful for the
+ * case of the key simply expiring on account of its TTL (use "lowTTL" instead).
* Default: WANObjectCache::GRACE_TTL_NONE.
- * - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds
- * ago, then try to have a single thread handle cache regeneration at any given time.
+ * - lockTSE: If the key is tombstoned or invalidated (by "checkKeys") less than this many
+ * seconds ago, try to have a single thread handle cache regeneration at any given time.
* Other threads will try to use stale values if possible. If, on miss, the time since
* 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.
+ * higher this is set, the higher the worst-case staleness can be. This option does not
+ * by itself handle the case of the key simply expiring on account of its TTL, so make
+ * sure that "lowTTL" is not disabled when using this option.
* Use WANObjectCache::TSE_NONE to disable this logic.
* Default: WANObjectCache::TSE_NONE.
* - busyValue: If no value exists and another thread is currently regenerating it, use this
* @note Callable type hints are not used to avoid class-autoloading
*/
final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
- $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
+ $pcTTL = $opts['pcTTL'] ?? self::TTL_UNCACHEABLE;
// Try the process cache if enabled and the cache callback is not within a cache callback.
// Process cache use in nested callbacks is not lag-safe with regard to HOLDOFF_TTL since
// the in-memory value is further lagged than the shared one since it uses a blind TTL.
if ( $pcTTL >= 0 && $this->callbackDepth == 0 ) {
- $group = isset( $opts['pcGroup'] ) ? $opts['pcGroup'] : self::PC_PRIMARY;
+ $group = $opts['pcGroup'] ?? self::PC_PRIMARY;
$procCache = $this->getProcessCache( $group );
$value = $procCache->get( $key );
} else {
* @note Callable type hints are not used to avoid class-autoloading
*/
protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts, &$asOf = null ) {
- $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
- $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
- $staleTTL = isset( $opts['staleTTL'] ) ? $opts['staleTTL'] : self::STALE_TTL_NONE;
- $graceTTL = isset( $opts['graceTTL'] ) ? $opts['graceTTL'] : self::GRACE_TTL_NONE;
- $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
- $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
- $popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
- $ageNew = isset( $opts['ageNew'] ) ? $opts['ageNew'] : self::AGE_NEW;
- $minTime = isset( $opts['minAsOf'] ) ? $opts['minAsOf'] : self::MIN_TIMESTAMP_NONE;
+ $lowTTL = $opts['lowTTL'] ?? min( self::LOW_TTL, $ttl );
+ $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
+ $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
+ $graceTTL = $opts['graceTTL'] ?? self::GRACE_TTL_NONE;
+ $checkKeys = $opts['checkKeys'] ?? [];
+ $busyValue = $opts['busyValue'] ?? null;
+ $popWindow = $opts['hotTTR'] ?? self::HOT_TTR;
+ $ageNew = $opts['ageNew'] ?? self::AGE_NEW;
+ $minTime = $opts['minAsOf'] ?? self::MIN_TIMESTAMP_NONE;
$versioned = isset( $opts['version'] );
// Get a collection name to describe this class of key
}
// Use the busy fallback value if nothing else
if ( $busyValue !== null ) {
- $this->stats->increment( "wanobjectcache.$kClass.miss.busy" );
+ $miss = is_infinite( $minTime ) ? 'renew' : 'miss';
+ $this->stats->increment( "wanobjectcache.$kClass.$miss.busy" );
return is_callable( $busyValue ) ? $busyValue() : $busyValue;
}
$this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, (int)$preCallbackTime - 60 );
}
- $this->stats->increment( "wanobjectcache.$kClass.miss.compute" );
+ $miss = is_infinite( $minTime ) ? 'renew' : 'miss';
+ $this->stats->increment( "wanobjectcache.$kClass.$miss.compute" );
return $value;
}
ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
) {
$valueKeys = array_keys( $keyedIds->getArrayCopy() );
- $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+ $checkKeys = $opts['checkKeys'] ?? [];
// Load required keys into process cache in one go
$this->warmupCache = $this->getRawKeysForWarmup(
) {
$idsByValueKey = $keyedIds->getArrayCopy();
$valueKeys = array_keys( $idsByValueKey );
- $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+ $checkKeys = $opts['checkKeys'] ?? [];
unset( $opts['lockTSE'] ); // incompatible
unset( $opts['busyValue'] ); // incompatible
* @since 1.27
*/
public function makeKey( $class, $component = null ) {
- return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
+ return $this->cache->makeKey( ...func_get_args() );
}
/**
* @since 1.27
*/
public function makeGlobalKey( $class, $component = null ) {
- return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
+ return $this->cache->makeGlobalKey( ...func_get_args() );
}
/**
*
* @param array|string|bool $wrapped
* @param float $now Unix Current timestamp (preferrably pre-query)
- * @return array (mixed; false if absent/tombstoned/invalid, current time left)
+ * @return array (mixed; false if absent/tombstoned/malformed, current time left)
*/
protected function unwrap( $wrapped, $now ) {
// Check if the value is a tombstone
return [ false, null ];
}
- $flags = isset( $wrapped[self::FLD_FLAGS] ) ? $wrapped[self::FLD_FLAGS] : 0;
+ $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];
protected function determineKeyClass( $key ) {
$parts = explode( ':', $key );
- return isset( $parts[1] ) ? $parts[1] : $parts[0]; // sanity
- }
-
- /**
- * @return float UNIX timestamp
- * @codeCoverageIgnore
- */
- protected function getCurrentTime() {
- return microtime( true );
+ return $parts[1] ?? $parts[0]; // sanity
}
/**
private function getNonProcessCachedKeys( array $keys, array $opts ) {
$keysFound = [];
if ( isset( $opts['pcTTL'] ) && $opts['pcTTL'] > 0 && $this->callbackDepth == 0 ) {
- $pcGroup = isset( $opts['pcGroup'] ) ? $opts['pcGroup'] : self::PC_PRIMARY;
+ $pcGroup = $opts['pcGroup'] ?? self::PC_PRIMARY;
$procCache = $this->getProcessCache( $pcGroup );
foreach ( $keys as $key ) {
if ( $procCache->get( $key ) !== false ) {
return $warmupCache;
}
+
+ /**
+ * @return float UNIX timestamp
+ * @codeCoverageIgnore
+ */
+ protected function getCurrentTime() {
+ return $this->wallClockOverride ?: microtime( true );
+ }
+
+ /**
+ * @param float|null &$time Mock UNIX timestamp for testing
+ * @codeCoverageIgnore
+ */
+ public function setMockTime( &$time ) {
+ $this->wallClockOverride =& $time;
+ $this->cache->setMockTime( $time );
+ }
}