objectcache: change "miss" to "renew" in metric name for preemptive refreshes
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
index 58d359c..0913322 100644 (file)
@@ -118,6 +118,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** @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() */
@@ -223,19 +226,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         */
        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;
        }
 
        /**
@@ -260,9 +259,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * 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().
@@ -301,10 +300,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $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;
        }
 
        /**
@@ -379,8 +378,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        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] ) ) {
@@ -389,9 +387,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                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 );
                                        }
                                }
@@ -417,7 +415,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                ? $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 );
@@ -494,10 +492,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         */
        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'] ) ) {
@@ -517,18 +515,18 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        // Case B: any long-running transaction; ignore this set()
                        } elseif ( $age > self::MAX_READ_LAG ) {
                                $this->logger->info( 'Rejected set() for {cachekey} due to snapshot lag.',
-                                       [ 'cachekey' => $key ] );
+                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
 
                                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 {cachekey} due to replication lag.',
-                                       [ 'cachekey' => $key ] );
+                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
                        // Case D: medium length request with medium replication lag; ignore this set()
                        } else {
                                $this->logger->info( 'Rejected set() for {cachekey} due to high read lag.',
-                                       [ 'cachekey' => $key ] );
+                                       [ 'cachekey' => $key, 'lag' => $lag, 'age' => $age ] );
 
                                return true; // no-op the write for being unsafe
                        }
@@ -782,7 +780,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * 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
@@ -981,23 +979,26 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *   - 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
@@ -1052,13 +1053,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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 {
@@ -1137,15 +1138,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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
@@ -1222,7 +1223,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                }
                                // 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;
                                }
@@ -1267,7 +1269,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        $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;
        }
@@ -1381,7 +1384,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                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(
@@ -1476,7 +1479,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        ) {
                $idsByValueKey = $keyedIds->getArrayCopy();
                $valueKeys = array_keys( $idsByValueKey );
-               $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+               $checkKeys = $opts['checkKeys'] ?? [];
                unset( $opts['lockTSE'] ); // incompatible
                unset( $opts['busyValue'] ); // incompatible
 
@@ -1581,7 +1584,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
                        $isStale = true;
                        $this->logger->warning( "Reaping stale check key '$key'." );
-                       $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, 1 );
+                       $ok = $this->cache->changeTTL( self::TIME_KEY_PREFIX . $key, self::TTL_SECOND );
                        if ( !$ok ) {
                                $this->logger->error( "Could not complete reap of check key '$key'." );
                        }
@@ -1602,7 +1605,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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() );
        }
 
        /**
@@ -1613,7 +1616,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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() );
        }
 
        /**
@@ -1824,7 +1827,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                'cmd' => 'set',
                                'key' => $key,
                                'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
-                               'ttl' => max( $ttl, 1 ),
+                               'ttl' => max( $ttl, self::TTL_SECOND ),
                                'sbt' => true, // substitute $UNIXTIME$ with actual microtime
                        ] );
 
@@ -2005,7 +2008,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * @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
@@ -2023,7 +2026,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        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];
@@ -2061,15 +2064,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        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
        }
 
        /**
@@ -2127,7 +2122,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        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 ) {
@@ -2173,4 +2168,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                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 );
+       }
 }