Improve MultiWriteBagOStuff::merge() cross-DC performance
[lhc/web/wiklou.git] / includes / objectcache / MultiWriteBagOStuff.php
index b690772..98380d9 100644 (file)
@@ -33,6 +33,8 @@ class MultiWriteBagOStuff extends BagOStuff {
        protected $caches;
        /** @var bool Use async secondary writes */
        protected $asyncWrites = false;
+       /** @var callback|null */
+       protected $asyncHandler;
 
        /** Idiom for "write to all backends" */
        const ALL = INF;
@@ -41,23 +43,23 @@ class MultiWriteBagOStuff extends BagOStuff {
 
        /**
         * $params include:
-        *   - caches:      This should have a numbered array of cache parameter
-        *                  structures, in the style required by $wgObjectCaches. See
-        *                  the documentation of $wgObjectCaches for more detail.
-        *                  BagOStuff objects can also be used as values.
-        *                  The first cache is the primary one, being the first to
-        *                  be read in the fallback chain. Writes happen to all stores
-        *                  in the order they are defined. However, lock()/unlock() calls
-        *                  only use the primary store.
-        *   - replication: Either 'sync' or 'async'. This controls whether writes to
-        *                  secondary stores are deferred when possible. Async writes
-        *                  require the HHVM register_postsend_function() function.
-        *                  Async writes can increase the chance of some race conditions
-        *                  or cause keys to expire seconds later than expected. It is
-        *                  safe to use for modules when cached values: are immutable,
-        *                  invalidation uses logical TTLs, invalidation uses etag/timestamp
-        *                  validation against the DB, or merge() is used to handle races.
-        *
+        *   - caches: A numbered array of either ObjectFactory::getObjectFromSpec
+        *      arrays yeilding BagOStuff objects or direct BagOStuff objects.
+        *      If using the former, the 'args' field *must* be set.
+        *      The first cache is the primary one, being the first to
+        *      be read in the fallback chain. Writes happen to all stores
+        *      in the order they are defined. However, lock()/unlock() calls
+        *      only use the primary store.
+        *   - replication: Either 'sync' or 'async'. This controls whether writes
+        *      to secondary stores are deferred when possible. Async writes
+        *      require setting 'asyncCallback'. HHVM register_postsend_function() function.
+        *      Async writes can increase the chance of some race conditions
+        *      or cause keys to expire seconds later than expected. It is
+        *      safe to use for modules when cached values: are immutable,
+        *      invalidation uses logical TTLs, invalidation uses etag/timestamp
+        *      validation against the DB, or merge() is used to handle races.
+        *   - asyncHandler: callable that takes a callback and runs it after the
+        *      current web request ends. In CLI mode, it should run it immediately.
         * @param array $params
         * @throws InvalidArgumentException
         */
@@ -72,12 +74,28 @@ class MultiWriteBagOStuff extends BagOStuff {
 
                $this->caches = array();
                foreach ( $params['caches'] as $cacheInfo ) {
-                       $this->caches[] = ( $cacheInfo instanceof BagOStuff )
-                               ? $cacheInfo
-                               : ObjectCache::newFromParams( $cacheInfo );
+                       if ( $cacheInfo instanceof BagOStuff ) {
+                               $this->caches[] = $cacheInfo;
+                       } else {
+                               if ( !isset( $cacheInfo['args'] ) ) {
+                                       // B/C for when $cacheInfo was for ObjectCache::newFromParams().
+                                       // Callers intenting this to be for ObjectFactory::getObjectFromSpec
+                                       // should have set "args" per the docs above. Doings so avoids extra
+                                       // (likely harmless) params (factory/class/calls) ending up in "args".
+                                       $cacheInfo['args'] = array( $cacheInfo );
+                               }
+                               $this->caches[] = ObjectFactory::getObjectFromSpec( $cacheInfo );
+                       }
                }
 
-               $this->asyncWrites = isset( $params['replication'] ) && $params['replication'] === 'async';
+               $this->asyncHandler = isset( $params['asyncHandler'] )
+                       ? $params['asyncHandler']
+                       : null;
+               $this->asyncWrites = (
+                       isset( $params['replication'] ) &&
+                       $params['replication'] === 'async' &&
+                       is_callable( $this->asyncHandler )
+               );
        }
 
        /**
@@ -87,11 +105,18 @@ class MultiWriteBagOStuff extends BagOStuff {
                $this->doWrite( self::ALL, 'setDebug', $debug );
        }
 
-       public function get( $key, &$casToken = null, $flags = 0 ) {
+       protected function doGet( $key, $flags = 0 ) {
+               if ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) {
+                       // If the latest write was a delete(), we do NOT want to fallback
+                       // to the other tiers and possibly see the old value. Also, this
+                       // is used by mergeViaLock(), which only needs to hit the primary.
+                       return $this->caches[0]->get( $key, $flags );
+               }
+
                $misses = 0; // number backends checked
                $value = false;
                foreach ( $this->caches as $cache ) {
-                       $value = $cache->get( $key, $casToken, $flags );
+                       $value = $cache->get( $key, $flags );
                        if ( $value !== false ) {
                                break;
                        }
@@ -174,17 +199,6 @@ class MultiWriteBagOStuff extends BagOStuff {
                return $this->caches[0]->unlock( $key );
        }
 
-       /**
-        * @param string $key
-        * @param callable $callback Callback method to be executed
-        * @param int $exptime Either an interval in seconds or a unix timestamp for expiry
-        * @param int $attempts The amount of times to attempt a merge in case of failure
-        * @return bool Success
-        */
-       public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
-               return $this->doWrite( self::ALL, 'merge', $key, $callback, $exptime );
-       }
-
        public function getLastError() {
                return $this->caches[0]->getLastError();
        }
@@ -218,7 +232,8 @@ class MultiWriteBagOStuff extends BagOStuff {
                        } else {
                                // Secondary write in async mode: do not block this HTTP request
                                $logger = $this->logger;
-                               DeferredUpdates::addCallableUpdate(
+                               call_user_func(
+                                       $this->asyncHandler,
                                        function () use ( $cache, $method, $args, $logger ) {
                                                if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
                                                        $logger->warning( "Async $method op failed" );