objectcache: Add BagOStuff::READ_VERIFIED flag to get()
[lhc/web/wiklou.git] / includes / objectcache / MultiWriteBagOStuff.php
index bbfaa5e..b690772 100644 (file)
 class MultiWriteBagOStuff extends BagOStuff {
        /** @var BagOStuff[] */
        protected $caches;
+       /** @var bool Use async secondary writes */
+       protected $asyncWrites = false;
+
+       /** Idiom for "write to all backends" */
+       const ALL = INF;
+
+       const UPGRADE_TTL = 3600; // TTL when a key is copied to a higher cache tier
 
        /**
-        * Constructor. Parameters are:
-        *
-        *   - 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.
+        * $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.
         *
         * @param array $params
         * @throws InvalidArgumentException
         */
        public function __construct( $params ) {
                parent::__construct( $params );
-               if ( !isset( $params['caches'] ) ) {
-                       throw new InvalidArgumentException( __METHOD__ . ': the caches parameter is required' );
+
+               if ( empty( $params['caches'] ) || !is_array( $params['caches'] ) ) {
+                       throw new InvalidArgumentException(
+                               __METHOD__ . ': "caches" parameter must be an array of caches'
+                       );
                }
 
                $this->caches = array();
                foreach ( $params['caches'] as $cacheInfo ) {
-                       $this->caches[] = ObjectCache::newFromParams( $cacheInfo );
+                       $this->caches[] = ( $cacheInfo instanceof BagOStuff )
+                               ? $cacheInfo
+                               : ObjectCache::newFromParams( $cacheInfo );
                }
+
+               $this->asyncWrites = isset( $params['replication'] ) && $params['replication'] === 'async';
        }
 
        /**
         * @param bool $debug
         */
        public function setDebug( $debug ) {
-               $this->doWrite( 'setDebug', $debug );
+               $this->doWrite( self::ALL, 'setDebug', $debug );
        }
 
        public function get( $key, &$casToken = null, $flags = 0 ) {
+               $misses = 0; // number backends checked
+               $value = false;
                foreach ( $this->caches as $cache ) {
                        $value = $cache->get( $key, $casToken, $flags );
                        if ( $value !== false ) {
-                               return $value;
+                               break;
                        }
+                       ++$misses;
                }
-               return false;
+
+               if ( $value !== false
+                       && $misses > 0
+                       && ( $flags & self::READ_VERIFIED ) == self::READ_VERIFIED
+               ) {
+                       $this->doWrite( $misses, 'set', $key, $value, self::UPGRADE_TTL );
+               }
+
+               return $value;
        }
 
        /**
@@ -78,7 +115,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        public function set( $key, $value, $exptime = 0 ) {
-               return $this->doWrite( 'set', $key, $value, $exptime );
+               return $this->doWrite( self::ALL, 'set', $key, $value, $exptime );
        }
 
        /**
@@ -86,7 +123,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        public function delete( $key ) {
-               return $this->doWrite( 'delete', $key );
+               return $this->doWrite( self::ALL, 'delete', $key );
        }
 
        /**
@@ -96,7 +133,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        public function add( $key, $value, $exptime = 0 ) {
-               return $this->doWrite( 'add', $key, $value, $exptime );
+               return $this->doWrite( self::ALL, 'add', $key, $value, $exptime );
        }
 
        /**
@@ -105,7 +142,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool|null
         */
        public function incr( $key, $value = 1 ) {
-               return $this->doWrite( 'incr', $key, $value );
+               return $this->doWrite( self::ALL, 'incr', $key, $value );
        }
 
        /**
@@ -114,7 +151,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        public function decr( $key, $value = 1 ) {
-               return $this->doWrite( 'decr', $key, $value );
+               return $this->doWrite( self::ALL, 'decr', $key, $value );
        }
 
        /**
@@ -126,11 +163,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         */
        public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
                // Lock only the first cache, to avoid deadlocks
-               if ( isset( $this->caches[0] ) ) {
-                       return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
-               } else {
-                       return true;
-               }
+               return $this->caches[0]->lock( $key, $timeout, $expiry, $rclass );
        }
 
        /**
@@ -138,11 +171,7 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool
         */
        public function unlock( $key ) {
-               if ( isset( $this->caches[0] ) ) {
-                       return $this->caches[0]->unlock( $key );
-               } else {
-                       return true;
-               }
+               return $this->caches[0]->unlock( $key );
        }
 
        /**
@@ -153,33 +182,52 @@ class MultiWriteBagOStuff extends BagOStuff {
         * @return bool Success
         */
        public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) {
-               return $this->doWrite( 'merge', $key, $callback, $exptime );
+               return $this->doWrite( self::ALL, 'merge', $key, $callback, $exptime );
        }
 
        public function getLastError() {
-               return isset( $this->caches[0] ) ? $this->caches[0]->getLastError() : self::ERR_NONE;
+               return $this->caches[0]->getLastError();
        }
 
        public function clearLastError() {
-               if ( isset( $this->caches[0] ) ) {
-                       $this->caches[0]->clearLastError();
-               }
+               $this->caches[0]->clearLastError();
        }
 
        /**
+        * Apply a write method to the first $count backing caches
+        *
+        * @param integer $count
         * @param string $method
+        * @param mixed ...
         * @return bool
         */
-       protected function doWrite( $method /*, ... */ ) {
+       protected function doWrite( $count, $method /*, ... */ ) {
                $ret = true;
-               $args = func_get_args();
-               array_shift( $args );
+               $args = array_slice( func_get_args(), 2 );
 
-               foreach ( $this->caches as $cache ) {
-                       if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
-                               $ret = false;
+               foreach ( $this->caches as $i => $cache ) {
+                       if ( $i >= $count ) {
+                               break; // ignore the lower tiers
+                       }
+
+                       if ( $i == 0 || !$this->asyncWrites ) {
+                               // First store or in sync mode: write now and get result
+                               if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
+                                       $ret = false;
+                               }
+                       } else {
+                               // Secondary write in async mode: do not block this HTTP request
+                               $logger = $this->logger;
+                               DeferredUpdates::addCallableUpdate(
+                                       function () use ( $cache, $method, $args, $logger ) {
+                                               if ( !call_user_func_array( array( $cache, $method ), $args ) ) {
+                                                       $logger->warning( "Async $method op failed" );
+                                               }
+                                       }
+                               );
                        }
                }
+
                return $ret;
        }
 
@@ -198,6 +246,7 @@ class MultiWriteBagOStuff extends BagOStuff {
                                $ret = true;
                        }
                }
+
                return $ret;
        }
 }