Revert "objectcache: detect default getWithSetCallback() set options"
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
index 5f6e324..8d3c6d9 100644 (file)
@@ -88,6 +88,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** @var int ERR_* constant for the "last error" registry */
        protected $lastRelayError = self::ERR_NONE;
 
+       /** @var integer Callback stack depth for getWithSetCallback() */
+       private $callbackDepth = 0;
+       /** @var mixed[] Temporary warm-up cache */
+       private $warmupCache = [];
+
        /** 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() */
@@ -284,7 +289,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                }
 
                // Fetch all of the raw values
-               $wrappedValues = $this->cache->getMulti( array_merge( $valueKeys, $checkKeysFlat ) );
+               $keysGet = array_merge( $valueKeys, $checkKeysFlat );
+               if ( $this->warmupCache ) {
+                       $wrappedValues = array_intersect_key( $this->warmupCache, array_flip( $keysGet ) );
+                       $keysGet = array_diff( $keysGet, array_keys( $wrappedValues ) ); // keys left to fetch
+               } else {
+                       $wrappedValues = [];
+               }
+               $wrappedValues += $this->cache->getMulti( $keysGet );
                // Time used to compare/init "check" keys (derived after getMulti() to be pessimistic)
                $now = microtime( true );
 
@@ -837,8 +849,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        final public function getWithSetCallback( $key, $ttl, $callback, array $opts = [] ) {
                $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
 
-               // Try the process cache if enabled
-               if ( $pcTTL >= 0 ) {
+               // 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;
                        $procCache = $this->getProcessCache( $group );
                        $value = $procCache->get( $key );
@@ -929,12 +943,13 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $cValue = $this->get( $key, $curTTL, $checkKeys, $asOf ); // current value
                $value = $cValue; // return value
 
-               // Determine if a regeneration is desired
+               $preCallbackTime = microtime( true );
+               // Determine if a cached value regeneration is needed or desired
                if ( $value !== false
                        && $curTTL > 0
                        && $this->isValid( $value, $versioned, $asOf, $minTime )
                        && !$this->worthRefreshExpiring( $curTTL, $lowTTL )
-                       && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow )
+                       && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime )
                ) {
                        return $value;
                }
@@ -984,7 +999,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                // Generate the new value from the callback...
                $setOpts = [];
-               $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
+               ++$this->callbackDepth;
+               try {
+                       $value = call_user_func_array( $callback, [ $cValue, &$ttl, &$setOpts, $asOf ] );
+               } finally {
+                       --$this->callbackDepth;
+               }
                // When delete() is called, writes are write-holed by the tombstone,
                // so use a special INTERIM key to pass the new value around threads.
                if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
@@ -1003,8 +1023,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                }
 
                if ( $value !== false && $ttl >= 0 ) {
-                       // Update the cache; this will fail if the key is tombstoned
                        $setOpts['lockTSE'] = $lockTSE;
+                       // Use best known "since" timestamp if not provided
+                       $setOpts += [ 'since' => $preCallbackTime ];
+                       // Update the cache; this will fail if the key is tombstoned
                        $this->set( $key, $value, $ttl, $setOpts );
                }
 
@@ -1016,6 +1038,95 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return $value;
        }
 
+       /**
+        * Method to fetch/regenerate multiple cache keys at once
+        *
+        * This works the same as getWithSetCallback() except:
+        *   - a) The $keys argument expects the result of WANObjectCache::makeMultiKeys()
+        *   - b) The $callback argument expects a callback taking the following arguments:
+        *         - $id: ID of an entity to query
+        *         - $oldValue : the prior cache value or false if none was present
+        *         - &$ttl : a reference to the new value TTL in seconds
+        *         - &$setOpts : a reference to options for set() which can be altered
+        *         - $oldAsOf : generation UNIX timestamp of $oldValue or null if not present
+        *        Aside from the additional $id argument, the other arguments function the same
+        *        way they do in getWithSetCallback().
+        *   - c) The return value is a map of (cache key => value) in the order of $keyedIds
+        *
+        * @see WANObjectCache::getWithSetCallback()
+        *
+        * Example usage:
+        * @code
+        *     $rows = $cache->getMultiWithSetCallback(
+        *         // Map of cache keys to entity IDs
+        *         $cache->makeMultiKeys(
+        *             $this->fileVersionIds(),
+        *             function ( $id, WANObjectCache $cache ) {
+        *                 return $cache->makeKey( 'file-version', $id );
+        *             }
+        *         ),
+        *         // Time-to-live (in seconds)
+        *         $cache::TTL_DAY,
+        *         // Function that derives the new key value
+        *         return function ( $id, $oldValue, &$ttl, array &$setOpts ) {
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
+        *             $setOpts += Database::getCacheSetOptions( $dbr );
+        *
+        *             // Load the row for this file
+        *             $row = $dbr->selectRow( 'file', '*', [ 'id' => $id ], __METHOD__ );
+        *
+        *             return $row ? (array)$row : false;
+        *         },
+        *         [
+        *             // Process cache for 30 seconds
+        *             'pcTTL' => 30,
+        *             // Use a dedicated 500 item cache (initialized on-the-fly)
+        *             'pcGroup' => 'file-versions:500'
+        *         ]
+        *     );
+        *     $files = array_map( [ __CLASS__, 'newFromRow' ], $rows );
+        * @endcode
+        *
+        * @param ArrayIterator $keyedIds Result of WANObjectCache::makeMultiKeys()
+        * @param integer $ttl Seconds to live for key updates
+        * @param callable $callback Callback the yields entity regeneration callbacks
+        * @param array $opts Options map
+        * @return array Map of (cache key => value) in the same order as $keyedIds
+        * @since 1.28
+        */
+       final public function getMultiWithSetCallback(
+               ArrayIterator $keyedIds, $ttl, callable $callback, array $opts = []
+       ) {
+               $keysWarmUp = iterator_to_array( $keyedIds, true );
+               $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
+               foreach ( $checkKeys as $i => $checkKeyOrKeys ) {
+                       if ( is_int( $i ) ) {
+                               $keysWarmUp[] = $checkKeyOrKeys;
+                       } else {
+                               $keysWarmUp = array_merge( $keysWarmUp, $checkKeyOrKeys );
+                       }
+               }
+
+               $this->warmupCache = $this->cache->getMulti( $keysWarmUp );
+               $this->warmupCache += array_fill_keys( $keysWarmUp, false );
+
+               // Wrap $callback to match the getWithSetCallback() format while passing $id to $callback
+               $id = null;
+               $func = function ( $oldValue, &$ttl, array $setOpts, $oldAsOf ) use ( $callback, &$id ) {
+                       return $callback( $id, $oldValue, $ttl, $setOpts, $oldAsOf );
+               };
+
+               $values = [];
+               foreach ( $keyedIds as $key => $id ) {
+                       $values[$key] = $this->getWithSetCallback( $key, $ttl, $func, $opts );
+               }
+
+               $this->warmupCache = [];
+
+               return $values;
+       }
+
        /**
         * @see BagOStuff::makeKey()
         * @param string ... Key component
@@ -1036,6 +1147,21 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return call_user_func_array( [ $this->cache, __FUNCTION__ ], func_get_args() );
        }
 
+       /**
+        * @param array $entities List of entity IDs
+        * @param callable $keyFunc Callback yielding a key from (entity ID, this WANObjectCache)
+        * @return ArrayIterator Iterator yielding (cache key => entity ID) in $entities order
+        * @since 1.28
+        */
+       public function makeMultiKeys( array $entities, callable $keyFunc ) {
+               $map = [];
+               foreach ( $entities as $entity ) {
+                       $map[$keyFunc( $entity, $this )] = $entity;
+               }
+
+               return new ArrayIterator( $map );
+       }
+
        /**
         * Get the "last error" registered; clearLastError() should be called manually
         * @return int ERR_* class constant for the "last error" registry
@@ -1222,10 +1348,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param float $asOf UNIX timestamp of the value
         * @param integer $ageNew Age of key when this might recommend refreshing (seconds)
         * @param integer $timeTillRefresh Age of key when it should be refreshed if popular (seconds)
+        * @param float $now The current UNIX timestamp
         * @return bool
         */
-       protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh ) {
-               $age = microtime( true ) - $asOf;
+       protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) {
+               $age = $now - $asOf;
                $timeOld = $age - $ageNew;
                if ( $timeOld <= 0 ) {
                        return false;