Merge "MovePage: Fix old, old bug with moving over redirects"
[lhc/web/wiklou.git] / includes / libs / objectcache / BagOStuff.php
index a7e8a47..5472e83 100644 (file)
@@ -44,9 +44,9 @@ use Psr\Log\NullLogger;
  */
 abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        /** @var array[] Lock tracking */
-       protected $locks = array();
+       protected $locks = [];
 
-       /** @var integer */
+       /** @var integer ERR_* class constant */
        protected $lastError = self::ERR_NONE;
 
        /** @var string */
@@ -55,9 +55,24 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        /** @var LoggerInterface */
        protected $logger;
 
+       /** @var callback|null */
+       protected $asyncHandler;
+
        /** @var bool */
        private $debugMode = false;
 
+       /** @var array */
+       private $duplicateKeyLookups = [];
+
+       /** @var bool */
+       private $reportDupes = false;
+
+       /** @var bool */
+       private $dupeTrackScheduled = false;
+
+       /** @var integer[] Map of (ATTR_* class constant => QOS_* class constant) */
+       protected $attrMap = [];
+
        /** Possible values for getLastError() */
        const ERR_NONE = 0; // no error
        const ERR_NO_RESPONSE = 1; // no response
@@ -71,7 +86,17 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
        const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
 
-       public function __construct( array $params = array() ) {
+       /**
+        * $params include:
+        *   - logger: Psr\Log\LoggerInterface instance
+        *   - keyspace: Default keyspace for $this->makeKey()
+        *   - asyncHandler: Callable to use for scheduling tasks after the web request ends.
+        *      In CLI mode, it should run the task immediately.
+        *   - reportDupes: Whether to emit warning log messages for all keys that were
+        *      requested more than once (requires an asyncHandler).
+        * @param array $params
+        */
+       public function __construct( array $params = [] ) {
                if ( isset( $params['logger'] ) ) {
                        $this->setLogger( $params['logger'] );
                } else {
@@ -81,6 +106,14 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                if ( isset( $params['keyspace'] ) ) {
                        $this->keyspace = $params['keyspace'];
                }
+
+               $this->asyncHandler = isset( $params['asyncHandler'] )
+                       ? $params['asyncHandler']
+                       : null;
+
+               if ( !empty( $params['reportDupes'] ) && is_callable( $this->asyncHandler ) ) {
+                       $this->reportDupes = true;
+               }
        }
 
        /**
@@ -144,9 +177,44 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                // B/C for ( $key, &$casToken = null, $flags = 0 )
                $flags = is_int( $oldFlags ) ? $oldFlags : $flags;
 
+               $this->trackDuplicateKeys( $key );
+
                return $this->doGet( $key, $flags );
        }
 
+       /**
+        * Track the number of times that a given key has been used.
+        * @param string $key
+        */
+       private function trackDuplicateKeys( $key ) {
+               if ( !$this->reportDupes ) {
+                       return;
+               }
+
+               if ( !isset( $this->duplicateKeyLookups[$key] ) ) {
+                       // Track that we have seen this key. This N-1 counting style allows
+                       // easy filtering with array_filter() later.
+                       $this->duplicateKeyLookups[$key] = 0;
+               } else {
+                       $this->duplicateKeyLookups[$key] += 1;
+
+                       if ( $this->dupeTrackScheduled === false ) {
+                               $this->dupeTrackScheduled = true;
+                               // Schedule a callback that logs keys processed more than once by get().
+                               call_user_func( $this->asyncHandler, function () {
+                                       $dups = array_filter( $this->duplicateKeyLookups );
+                                       foreach ( $dups as $key => $count ) {
+                                               $this->logger->warning(
+                                                       'Duplicate get(): "{key}" fetched {count} times',
+                                                       // Count is N-1 of the actual lookup count
+                                                       [ 'key' => $key, 'count' => $count + 1, ]
+                                               );
+                                       }
+                               } );
+                       }
+               }
+       }
+
        /**
         * @param string $key
         * @param integer $flags Bitfield of BagOStuff::READ_* constants [optional]
@@ -187,10 +255,12 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        abstract public function delete( $key );
 
        /**
-        * Merge changes into the existing cache value (possibly creating a new one).
+        * Merge changes into the existing cache value (possibly creating a new one)
+        *
         * The callback function returns the new value given the current value
         * (which will be false if not present), and takes the arguments:
-        * (this BagOStuff, cache key, current value).
+        * (this BagOStuff, cache key, current value, TTL).
+        * The TTL parameter is reference set to $exptime. It can be overriden in the callback.
         *
         * @param string $key
         * @param callable $callback Callback method to be executed
@@ -200,11 +270,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         * @throws InvalidArgumentException
         */
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
-               if ( !is_callable( $callback ) ) {
-                       throw new InvalidArgumentException( "Got invalid callback." );
-               }
-
+       public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                return $this->mergeViaLock( $key, $callback, $exptime, $attempts, $flags );
        }
 
@@ -220,14 +286,18 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
                do {
                        $this->clearLastError();
+                       $reportDupes = $this->reportDupes;
+                       $this->reportDupes = false;
                        $casToken = null; // passed by reference
                        $currentValue = $this->getWithToken( $key, $casToken, self::READ_LATEST );
+                       $this->reportDupes = $reportDupes;
+
                        if ( $this->getLastError() ) {
                                return false; // don't spam retries (retry only on races)
                        }
 
                        // Derive the new value from the old value
-                       $value = call_user_func( $callback, $this, $key, $currentValue );
+                       $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
 
                        $this->clearLastError();
                        if ( $value === false ) {
@@ -277,12 +347,16 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                }
 
                $this->clearLastError();
+               $reportDupes = $this->reportDupes;
+               $this->reportDupes = false;
                $currentValue = $this->get( $key, self::READ_LATEST );
+               $this->reportDupes = $reportDupes;
+
                if ( $this->getLastError() ) {
                        $success = false;
                } else {
                        // Derive the new value from the old value
-                       $value = call_user_func( $callback, $this, $key, $currentValue );
+                       $value = call_user_func( $callback, $this, $key, $currentValue, $exptime );
                        if ( $value === false ) {
                                $success = true; // do nothing
                        } else {
@@ -298,6 +372,20 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                return $success;
        }
 
+       /**
+        * Reset the TTL on a key if it exists
+        *
+        * @param string $key
+        * @param int $expiry
+        * @return bool Success Returns false if there is no key
+        * @since 1.28
+        */
+       public function changeTTL( $key, $expiry = 0 ) {
+               $value = $this->get( $key );
+
+               return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
+       }
+
        /**
         * Acquire an advisory lock on a key string
         *
@@ -351,7 +439,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                }
 
                if ( $locked ) {
-                       $this->locks[$key] = array( 'class' => $rclass, 'depth' => 1 );
+                       $this->locks[$key] = [ 'class' => $rclass, 'depth' => 1 ];
                }
 
                return $locked;
@@ -430,7 +518,7 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         * @return array
         */
        public function getMulti( array $keys, $flags = 0 ) {
-               $res = array();
+               $res = [];
                foreach ( $keys as $key ) {
                        $val = $this->get( $key );
                        if ( $val !== false ) {
@@ -577,9 +665,9 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         */
        protected function debug( $text ) {
                if ( $this->debugMode ) {
-                       $this->logger->debug( "{class} debug: $text", array(
+                       $this->logger->debug( "{class} debug: $text", [
                                'class' => get_class( $this ),
-                       ) );
+                       ] );
                }
        }
 
@@ -663,4 +751,34 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
        public function makeKey() {
                return $this->makeKeyInternal( $this->keyspace, func_get_args() );
        }
+
+       /**
+        * @param integer $flag ATTR_* class constant
+        * @return integer QOS_* class constant
+        * @since 1.28
+        */
+       public function getQoS( $flag ) {
+               return isset( $this->attrMap[$flag] ) ? $this->attrMap[$flag] : self::QOS_UNKNOWN;
+       }
+
+       /**
+        * Merge the flag maps of one or more BagOStuff objects into a "lowest common denominator" map
+        *
+        * @param BagOStuff[] $bags
+        * @return integer[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
+        */
+       protected function mergeFlagMaps( array $bags ) {
+               $map = [];
+               foreach ( $bags as $bag ) {
+                       foreach ( $bag->attrMap as $attr => $rank ) {
+                               if ( isset( $map[$attr] ) ) {
+                                       $map[$attr] = min( $map[$attr], $rank );
+                               } else {
+                                       $map[$attr] = $rank;
+                               }
+                       }
+               }
+
+               return $map;
+       }
 }