objectcache: add another example case to WANObjectCache::getWithSetCallback()
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
index 68da16d..36b45a1 100644 (file)
@@ -225,8 +225,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         */
        public static function newEmpty() {
                return new static( [
-                       'cache'   => new EmptyBagOStuff(),
-                       'pool'    => 'empty'
+                       'cache'   => new EmptyBagOStuff()
                ] );
        }
 
@@ -387,13 +386,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $purgeValues = [];
                foreach ( $timeKeys as $timeKey ) {
                        $purge = isset( $wrappedValues[$timeKey] )
-                               ? self::parsePurgeValue( $wrappedValues[$timeKey] )
+                               ? $this->parsePurgeValue( $wrappedValues[$timeKey] )
                                : false;
                        if ( $purge === false ) {
                                // Key is not set or invalid; regenerate
                                $newVal = $this->makePurgeValue( $now, self::HOLDOFF_TTL );
                                $this->cache->add( $timeKey, $newVal, self::CHECK_KEY_TTL );
-                               $purge = self::parsePurgeValue( $newVal );
+                               $purge = $this->parsePurgeValue( $newVal );
                        }
                        $purgeValues[] = $purge;
                }
@@ -611,9 +610,58 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * Fetch the values of each timestamp "check" key
         *
         * This works like getCheckKeyTime() except it takes a list of keys
-        * and returns a list of timestamps instead of just that of one key
+        * and returns a map of timestamps instead of just that of one key
+        *
+        * This might be useful if both:
+        *   - a) a class of entities each depend on hundreds of other entities
+        *   - b) these other entities are depended upon by millions of entities
+        *
+        * The later entities can each use a "check" key to invalidate their dependee entities.
+        * However, it is expensive for the former entities to verify against all of the relevant
+        * "check" keys during each getWithSetCallback() call. A less expensive approach is to do
+        * these verifications only after a "time-till-verify" (TTV) has passed. This is a middle
+        * ground between using blind TTLs and using constant verification. The adaptiveTTL() method
+        * can be used to dynamically adjust the TTV. Also, the initial TTV can make use of the
+        * last-modified times of the dependant entities (either from the DB or the "check" keys).
+        *
+        * Example usage:
+        * @code
+        *     $value = $cache->getWithSetCallback(
+        *         $cache->makeGlobalKey( 'wikibase-item', $id ),
+        *         self::INITIAL_TTV, // initial time-till-verify
+        *         function ( $oldValue, &$ttv, &$setOpts, $oldAsOf ) use ( $checkKeys, $cache ) {
+        *             $now = microtime( true );
+        *             // Use $oldValue if it passes max ultimate age and "check" key comparisons
+        *             if ( $oldValue &&
+        *                 $oldAsOf > max( $cache->getMultiCheckKeyTime( $checkKeys ) ) &&
+        *                 ( $now - $oldValue['ctime'] ) <= self::MAX_CACHE_AGE
+        *             ) {
+        *                 // Increase time-till-verify by 50% of last time to reduce overhead
+        *                 $ttv = $cache->adaptiveTTL( $oldAsOf, self::MAX_TTV, self::MIN_TTV, 1.5 );
+        *                 // Unlike $oldAsOf, "ctime" is the ultimate age of the cached data
+        *                 return $oldValue;
+        *             }
+        *
+        *             $mtimes = []; // dependency last-modified times; passed by reference
+        *             $value = [ 'data' => $this->fetchEntityData( $mtimes ), 'ctime' => $now ];
+        *             // Guess time-till-change among the dependencies, e.g. 1/(total change rate)
+        *             $ttc = 1 / array_sum( array_map(
+        *                 function ( $mtime ) use ( $now ) {
+        *                     return 1 / ( $mtime ? ( $now - $mtime ) : 900 );
+        *                 },
+        *                 $mtimes
+        *             ) );
+        *             // The time-to-verify should not be overly pessimistic nor optimistic
+        *             $ttv = min( max( $ttc, self::MIN_TTV ), self::MAX_TTV );
+        *
+        *             return $value;
+        *         },
+        *         [ 'staleTTL' => $cache::TTL_DAY ] // keep around to verify and re-save
+        *     );
+        * @endcode
         *
         * @see WANObjectCache::getCheckKeyTime()
+        * @see WANObjectCache::getWithSetCallback()
         *
         * @param array $keys
         * @return float[] Map of (key => UNIX timestamp)
@@ -628,10 +676,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $rawValues = $this->cache->getMulti( $rawKeys );
                $rawValues += array_fill_keys( $rawKeys, false );
 
-               $index = 0;
                $times = [];
                foreach ( $rawKeys as $key => $rawKey ) {
-                       $purge = self::parsePurgeValue( $rawValues[$rawKey] );
+                       $purge = $this->parsePurgeValue( $rawValues[$rawKey] );
                        if ( $purge !== false ) {
                                $time = $purge[self::FLD_TIME];
                        } else {
@@ -853,6 +900,40 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *     );
         * @endcode
         *
+        * Example usage (key holding an LRU subkey:value map; this can avoid flooding cache with
+        * keys for an unlimited set of (constraint,situation) pairs, thereby avoiding elevated
+        * cache evictions and wasted memory):
+        * @code
+        *     $catSituationTolerabilityCache = $this->cache->getWithSetCallback(
+        *         // Group by constraint ID/hash, cat family ID/hash, or something else useful
+        *         $this->cache->makeKey( 'cat-situation-tolerablity-checks', $groupKey ),
+        *         WANObjectCache::TTL_DAY, // rarely used groups should fade away
+        *         // The $scenarioKey format is $constraintId:<ID/hash of $situation>
+        *         function ( $cacheMap ) use ( $scenarioKey, $constraintId, $situation ) {
+        *             $lruCache = MapCacheLRU::newFromArray( $cacheMap ?: [], self::CACHE_SIZE );
+        *             $result = $lruCache->get( $scenarioKey ); // triggers LRU bump if present
+        *             if ( $result === null || $this->isScenarioResultExpired( $result ) ) {
+        *                 $result = $this->checkScenarioTolerability( $constraintId, $situation );
+        *                 $lruCache->set( $scenarioKey, $result, 3 / 8 );
+        *             }
+        *             // Save the new LRU cache map and reset the map's TTL
+        *             return $lruCache->toArray();
+        *         },
+        *         [
+        *             // Once map is > 1 sec old, consider refreshing
+        *             'ageNew' => 1,
+        *             // Update within 5 seconds after "ageNew" given a 1hz cache check rate
+        *             'hotTTR' => 5,
+        *             // Avoid querying cache servers multiple times in a request; this also means
+        *             // that a request can only alter the value of any given constraint key once
+        *             'pcTTL' => WANObjectCache::TTL_PROC_LONG
+        *         ]
+        *     );
+        *     $tolerability = isset( $catSituationTolerabilityCache[$scenarioKey] )
+        *         ? $catSituationTolerabilityCache[$scenarioKey]
+        *         : $this->checkScenarioTolerability( $constraintId, $situation );
+        * @endcode
+        *
         * @see WANObjectCache::get()
         * @see WANObjectCache::set()
         *
@@ -1428,7 +1509,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         * @since 1.28
         */
-       public function reap( $key, $purgeTimestamp, &$isStale = false ) {
+       final public function reap( $key, $purgeTimestamp, &$isStale = false ) {
                $minAsOf = $purgeTimestamp + self::HOLDOFF_TTL;
                $wrapped = $this->cache->get( self::VALUE_KEY_PREFIX . $key );
                if ( is_array( $wrapped ) && $wrapped[self::FLD_TIME] < $minAsOf ) {
@@ -1457,7 +1538,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         * @since 1.28
         */
-       public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
+       final public function reapCheckKey( $key, $purgeTimestamp, &$isStale = false ) {
                $purge = $this->parsePurgeValue( $this->cache->get( self::TIME_KEY_PREFIX . $key ) );
                if ( $purge && $purge[self::FLD_TIME] < $purgeTimestamp ) {
                        $isStale = true;
@@ -1503,7 +1584,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
         * @since 1.28
         */
-       public function makeMultiKeys( array $entities, callable $keyFunc ) {
+       final public function makeMultiKeys( array $entities, callable $keyFunc ) {
                $map = [];
                foreach ( $entities as $entity ) {
                        $map[$keyFunc( $entity, $this )] = $entity;
@@ -1576,7 +1657,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param bool $enabled Whether to enable interim caching
         * @since 1.31
         */
-       public function useInterimHoldOffCaching( $enabled ) {
+       final public function useInterimHoldOffCaching( $enabled ) {
                $this->useInterimHoldOffCaching = $enabled;
        }
 
@@ -1631,7 +1712,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *                 $ttl = ( $newList === $oldValue )
         *                     // No change: cache for 150% of the age of $oldValue
         *                     ? $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, 1.5 )
-        *                     // Changed: cache for %50 of the age of $oldValue
+        *                     // Changed: cache for 50% of the age of $oldValue
         *                     : $cache->adaptiveTTL( $oldAsOf, $maxTTL, $minTTL, .5 );
         *             }
         *
@@ -1670,7 +1751,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return int Number of warmup key cache misses last round
         * @since 1.30
         */
-       public function getWarmupKeyMisses() {
+       final public function getWarmupKeyMisses() {
                return $this->warmupKeyMisses;
        }
 
@@ -1877,7 +1958,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         */
        protected function unwrap( $wrapped, $now ) {
                // Check if the value is a tombstone
-               $purge = self::parsePurgeValue( $wrapped );
+               $purge = $this->parsePurgeValue( $wrapped );
                if ( $purge !== false ) {
                        // Purged values should always have a negative current $ttl
                        $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
@@ -1945,7 +2026,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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 ) {
+       protected function parsePurgeValue( $value ) {
                if ( !is_string( $value ) ) {
                        return false;
                }