objectcache: Add TTL_INDEFINITE to IExpiringStore
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
index 71c8a93..7e3fa4f 100644 (file)
  * @author Aaron Schulz
  */
 
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
 /**
  * Multi-datacenter aware caching interface
  *
@@ -60,7 +64,7 @@
  * @ingroup Cache
  * @since 1.26
  */
-class WANObjectCache {
+class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** @var BagOStuff The local datacenter cache */
        protected $cache;
        /** @var HashBagOStuff Script instance PHP cache */
@@ -69,30 +73,28 @@ class WANObjectCache {
        protected $pool;
        /** @var EventRelayer Bus that handles purge broadcasts */
        protected $relayer;
+       /** @var LoggerInterface */
+       protected $logger;
 
        /** @var int ERR_* constant for the "last error" registry */
        protected $lastRelayError = self::ERR_NONE;
 
        /** Max time expected to pass between delete() and DB commit finishing */
        const MAX_COMMIT_DELAY = 3;
-       /** Max replication lag before applying TTL_LAGGED to set() */
-       const MAX_REPLICA_LAG = 5;
-       /** Max time since snapshot transaction start to avoid no-op of set() */
-       const MAX_SNAPSHOT_LAG = 5;
+       /** Max replication+snapshot lag before applying TTL_LAGGED or disallowing set() */
+       const MAX_READ_LAG = 7;
        /** Seconds to tombstone keys on delete() */
-       const HOLDOFF_TTL = 14; // MAX_COMMIT_DELAY + MAX_REPLICA_LAG + MAX_SNAPSHOT_LAG + 1
+       const HOLDOFF_TTL = 11; // MAX_COMMIT_DELAY + MAX_READ_LAG + 1
 
        /** Seconds to keep dependency purge keys around */
-       const CHECK_KEY_TTL = 31536000; // 1 year
+       const CHECK_KEY_TTL = self::TTL_YEAR;
        /** Seconds to keep lock keys around */
-       const LOCK_TTL = 5;
+       const LOCK_TTL = 10;
        /** Default remaining TTL at which to consider pre-emptive regeneration */
        const LOW_TTL = 30;
        /** Default time-since-expiry on a miss that makes a key "hot" */
        const LOCK_TSE = 1;
 
-       /** Idiom for set()/getWithSetCallback() TTL being "forever" */
-       const TTL_NONE = 0;
        /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
        const TTL_UNCACHEABLE = -1;
        /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
@@ -100,6 +102,9 @@ class WANObjectCache {
        /** Max TTL to store keys when a data sourced is lagged */
        const TTL_LAGGED = 30;
 
+       /** Tiny negative float to use when CTL comes up >= 0 due to clock skew */
+       const TINY_NEGATIVE = -0.000001;
+
        /** Cache format version number */
        const VERSION = 1;
 
@@ -107,6 +112,10 @@ class WANObjectCache {
        const FLD_VALUE = 1;
        const FLD_TTL = 2;
        const FLD_TIME = 3;
+       const FLD_FLAGS = 4;
+
+       /** @var integer Treat this value as expired-on-arrival */
+       const FLG_STALE = 1;
 
        const ERR_NONE = 0; // no error
        const ERR_NO_RESPONSE = 1; // no response
@@ -120,17 +129,25 @@ class WANObjectCache {
 
        const PURGE_VAL_PREFIX = 'PURGED:';
 
+       const MAX_PC_KEYS = 1000; // max keys to keep in process cache
+
        /**
         * @param array $params
         *   - cache   : BagOStuff object
         *   - pool    : pool name
         *   - relayer : EventRelayer object
+        *   - logger  : LoggerInterface object
         */
        public function __construct( array $params ) {
                $this->cache = $params['cache'];
                $this->pool = $params['pool'];
                $this->relayer = $params['relayer'];
-               $this->procCache = new HashBagOStuff();
+               $this->procCache = new HashBagOStuff( array( 'maxKeys' => self::MAX_PC_KEYS ) );
+               $this->setLogger( isset( $params['logger'] ) ? $params['logger'] : new NullLogger() );
+       }
+
+       public function setLogger( LoggerInterface $logger ) {
+               $this->logger = $logger;
        }
 
        /**
@@ -149,11 +166,11 @@ class WANObjectCache {
        /**
         * Fetch the value of a key from cache
         *
-        * If passed in, $curTTL is set to the remaining TTL (current time left):
-        *   - a) INF; if the key exists, has no TTL, and is not expired by $checkKeys
-        *   - b) float (>=0); if the key exists, has a TTL, and is not expired by $checkKeys
-        *   - c) float (<0); if the key is tombstoned or existing but expired by $checkKeys
-        *   - d) null; if the key does not exist and is not tombstoned
+        * If supplied, $curTTL is set to the remaining TTL (current time left):
+        *   - a) INF; if $key exists, has no TTL, and is not expired by $checkKeys
+        *   - b) float (>=0); if $key exists, has a TTL, and is not expired by $checkKeys
+        *   - c) float (<0); if $key is tombstoned, stale, or existing but expired by $checkKeys
+        *   - d) null; if $key does not exist and is not tombstoned
         *
         * If a key is tombstoned, $curTTL will reflect the time since delete().
         *
@@ -283,13 +300,14 @@ class WANObjectCache {
         *     $setOpts = Database::getCacheSetOptions( $dbr );
         *     // Fetch the row from the DB
         *     $row = $dbr->selectRow( ... );
-        *     $key = wfMemcKey( 'building', $buildingId );
-        *     $cache->set( $key, $row, 86400, $setOpts );
+        *     $key = $cache->makeKey( 'building', $buildingId );
+        *     $cache->set( $key, $row, $cache::TTL_DAY, $setOpts );
         * @endcode
         *
         * @param string $key Cache key
         * @param mixed $value
-        * @param integer $ttl Seconds to live [0=forever]
+        * @param integer $ttl Seconds to live. Special values are:
+        *   - WANObjectCache::TTL_INDEFINITE: Cache forever
         * @param array $opts Options map:
         *   - lag     : Seconds of slave lag. Typically, this is either the slave lag
         *               before the data was read or, if applicable, the slave lag before
@@ -299,10 +317,13 @@ class WANObjectCache {
         *               the current time the data was read or (if applicable) the time when
         *               the snapshot-isolated transaction the data was read from started.
         *               Default: 0 seconds
-        *   - lockTSE : if excessive possible snapshot lag is detected,
-        *               then stash the value into a temporary location
-        *               with this TTL. This is only useful if the reads
-        *               use getWithSetCallback() with "lockTSE" set.
+        *   - pending : Whether this data is possibly from an uncommitted write transaction.
+        *               Generally, other threads should not see values from the future and
+        *               they certainly should not see ones that ended up getting rolled back.
+        *               Default: false
+        *   - lockTSE : if excessive replication/snapshot lag is detected, then store the value
+        *               with this TTL and flag it as stale. This is only useful if the reads for
+        *               this key use getWithSetCallback() with "lockTSE" set.
         *               Default: WANObjectCache::TSE_NONE
         * @return bool Success
         */
@@ -311,21 +332,39 @@ class WANObjectCache {
                $age = isset( $opts['since'] ) ? max( 0, microtime( true ) - $opts['since'] ) : 0;
                $lag = isset( $opts['lag'] ) ? $opts['lag'] : 0;
 
-               if ( $lag > self::MAX_REPLICA_LAG ) {
-                       // Too much lag detected; lower TTL so it converges faster
-                       $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
+               // Do not cache potentially uncommitted data as it might get rolled back
+               if ( !empty( $opts['pending'] ) ) {
+                       $this->logger->info( "Rejected set() for $key due to pending writes." );
+
+                       return true; // no-op the write for being unsafe
                }
 
-               if ( $age > self::MAX_SNAPSHOT_LAG ) {
+               $wrapExtra = array(); // additional wrapped value fields
+               // Check if there's a risk of writing stale data after the purge tombstone expired
+               if ( ( $lag + $age ) > self::MAX_READ_LAG ) {
+                       // Case A: read lag with "lockTSE"; save but record value as stale
                        if ( $lockTSE >= 0 ) {
-                               $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
-                               $this->cache->set( self::STASH_KEY_PREFIX . $key, $value, $tempTTL );
-                       }
+                               $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
+                               $wrapExtra[self::FLD_FLAGS] = self::FLG_STALE; // mark as stale
+                       // Case B: any long-running transaction; ignore this set()
+                       } elseif ( $age > self::MAX_READ_LAG ) {
+                               $this->logger->warning( "Rejected set() for $key due to snapshot lag." );
+
+                               return true; // no-op the write for being unsafe
+                       // Case C: high replication lag; lower TTL instead of ignoring all set()s
+                       } elseif ( $lag > self::MAX_READ_LAG ) {
+                               $ttl = $ttl ? min( $ttl, self::TTL_LAGGED ) : self::TTL_LAGGED;
+                               $this->logger->warning( "Lowered set() TTL for $key due to replication lag." );
+                       // Case D: medium length request with medium replication lag; ignore this set()
+                       } else {
+                               $this->logger->warning( "Rejected set() for $key due to high read lag." );
 
-                       return true; // no-op the write for being unsafe
+                               return true; // no-op the write for being unsafe
+                       }
                }
 
-               $wrapped = $this->wrap( $value, $ttl );
+               // Wrap that value with time/TTL/version metadata
+               $wrapped = $this->wrap( $value, $ttl ) + $wrapExtra;
 
                $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
                        return ( is_string( $cWrapped ) )
@@ -372,7 +411,7 @@ class WANObjectCache {
         *     ... <execute some stuff> ...
         *     // Update the row in the DB
         *     $dbw->update( ... );
-        *     $key = wfMemcKey( 'homes', $homeId );
+        *     $key = $cache->makeKey( 'homes', $homeId );
         *     // Purge the corresponding cache entry just before committing
         *     $dbw->onTransactionPreCommitOrIdle( function() use ( $cache, $key ) {
         *         $cache->delete( $key );
@@ -381,13 +420,16 @@ class WANObjectCache {
         *     $dbw->commit(); // end of request
         * @endcode
         *
-        * If called twice on the same key, then the last hold-off TTL takes
-        * precedence. For idempotence, the $ttl should not vary for different
-        * delete() calls on the same key. Also note that lowering $ttl reduces
-        * the effective range of the 'lockTSE' parameter to getWithSetCallback().
+        * The $ttl parameter can be used when purging values that have not actually changed
+        * recently. For example, a cleanup script to purge cache entries does not really need
+        * a hold-off period, so it can use the value 1. Likewise for user-requested purge.
+        * Note that $ttl limits the effective range of 'lockTSE' for getWithSetCallback().
+        *
+        * If called twice on the same key, then the last hold-off TTL takes precedence. For
+        * idempotence, the $ttl should not vary for different delete() calls on the same key.
         *
         * @param string $key Cache key
-        * @param integer $ttl How long to block writes to the key [seconds]
+        * @param integer $ttl Tombstone TTL; Default: WANObjectCache::HOLDOFF_TTL
         * @return bool True if the item was purged or not found, false on failure
         */
        final public function delete( $key, $ttl = self::HOLDOFF_TTL ) {
@@ -536,9 +578,9 @@ class WANObjectCache {
         * @code
         *     $catInfo = $cache->getWithSetCallback(
         *         // Key to store the cached value under
-        *         wfMemcKey( 'cat-attributes', $catId ),
-        *         // Time-to-live (seconds)
-        *         60,
+        *         $cache->makeKey( 'cat-attributes', $catId ),
+        *         // Time-to-live (in seconds)
+        *         $cache::TTL_MINUTE,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             $dbr = wfGetDB( DB_SLAVE );
@@ -554,9 +596,9 @@ class WANObjectCache {
         * @code
         *     $catConfig = $cache->getWithSetCallback(
         *         // Key to store the cached value under
-        *         wfMemcKey( 'site-cat-config' ),
-        *         // Time-to-live (seconds)
-        *         86400,
+        *         $cache->makeKey( 'site-cat-config' ),
+        *         // Time-to-live (in seconds)
+        *         $cache::TTL_DAY,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             $dbr = wfGetDB( DB_SLAVE );
@@ -567,7 +609,7 @@ class WANObjectCache {
         *         },
         *         array(
         *             // Calling touchCheckKey() on this key invalidates the cache
-        *             'checkKeys' => array( wfMemcKey( 'site-cat-config' ) ),
+        *             'checkKeys' => array( $cache->makeKey( 'site-cat-config' ) ),
         *             // Try to only let one datacenter thread manage cache updates at a time
         *             'lockTSE' => 30
         *         )
@@ -578,9 +620,9 @@ class WANObjectCache {
         * @code
         *     $catState = $cache->getWithSetCallback(
         *         // Key to store the cached value under
-        *         wfMemcKey( 'cat-state', $cat->getId() ),
+        *         $cache->makeKey( 'cat-state', $cat->getId() ),
         *         // Time-to-live (seconds)
-        *         900,
+        *         $cache::TTL_HOUR,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             // Determine new value from the DB
@@ -594,9 +636,9 @@ class WANObjectCache {
         *              // The "check" keys that represent things the value depends on;
         *              // Calling touchCheckKey() on any of them invalidates the cache
         *             'checkKeys' => array(
-        *                 wfMemcKey( 'sustenance-bowls', $cat->getRoomId() ),
-        *                 wfMemcKey( 'people-present', $cat->getHouseId() ),
-        *                 wfMemcKey( 'cat-laws', $cat->getCityId() ),
+        *                 $cache->makeKey( 'sustenance-bowls', $cat->getRoomId() ),
+        *                 $cache->makeKey( 'people-present', $cat->getHouseId() ),
+        *                 $cache->makeKey( 'cat-laws', $cat->getCityId() ),
         *             )
         *         )
         *     );
@@ -606,8 +648,8 @@ class WANObjectCache {
         * @code
         *     $lastCatActions = $cache->getWithSetCallback(
         *         // Key to store the cached value under
-        *         wfMemcKey( 'cat-last-actions', 100 ),
-        *         // Time-to-live (seconds)
+        *         $cache->makeKey( 'cat-last-actions', 100 ),
+        *         // Time-to-live (in seconds)
         *         10,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
@@ -633,7 +675,7 @@ class WANObjectCache {
         *
         * @param string $key Cache key
         * @param integer $ttl Seconds to live for key updates. Special values are:
-        *   - WANObjectCache::TTL_NONE : Cache forever
+        *   - WANObjectCache::TTL_INDEFINITE: Cache forever
         *   - WANObjectCache::TTL_UNCACHEABLE: Do not cache at all
         * @param callable $callback Value generation function
         * @param array $opts Options map:
@@ -656,26 +698,9 @@ class WANObjectCache {
         *      since the callback should use slave DBs and they may be lagged or have snapshot
         *      isolation anyway, this should not typically matter.
         *      Default: WANObjectCache::TTL_UNCACHEABLE.
-        * @param array $oldOpts Unused (mentioned only to avoid PHPDoc warnings)
         * @return mixed Value to use for the key
         */
-       final public function getWithSetCallback(
-               $key, $ttl, $callback, array $opts = array(), $oldOpts = array()
-       ) {
-               // Back-compat with 1.26: Swap $ttl and $callback
-               if ( is_int( $callback ) ) {
-                       $temp = $ttl;
-                       $ttl = $callback;
-                       $callback = $temp;
-               }
-               // Back-compat with 1.26: $checkKeys as separate parameter
-               if ( $oldOpts || ( is_array( $opts ) && isset( $opts[0] ) ) ) {
-                       $checkKeys = $opts;
-                       $opts = $oldOpts;
-               } else {
-                       $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : array();
-               }
-
+       final public function getWithSetCallback( $key, $ttl, $callback, array $opts = array() ) {
                $pcTTL = isset( $opts['pcTTL'] ) ? $opts['pcTTL'] : self::TTL_UNCACHEABLE;
 
                // Try the process cache if enabled
@@ -683,7 +708,7 @@ class WANObjectCache {
 
                if ( $value === false ) {
                        // Fetch the value over the network
-                       $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $checkKeys, $opts );
+                       $value = $this->doGetWithSetCallback( $key, $ttl, $callback, $opts );
                        // Update the process cache if enabled
                        if ( $pcTTL >= 0 && $value !== false ) {
                                $this->procCache->set( $key, $value, $pcTTL );
@@ -701,15 +726,13 @@ class WANObjectCache {
         * @param string $key
         * @param integer $ttl
         * @param callback $callback
-        * @param array $checkKeys
         * @param array $opts
         * @return mixed
         */
-       protected function doGetWithSetCallback(
-               $key, $ttl, $callback, array $checkKeys, array $opts
-       ) {
+       protected function doGetWithSetCallback( $key, $ttl, $callback, array $opts ) {
                $lowTTL = isset( $opts['lowTTL'] ) ? $opts['lowTTL'] : min( self::LOW_TTL, $ttl );
                $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
+               $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : array();
 
                // Get the current key value
                $curTTL = null;
@@ -778,6 +801,26 @@ class WANObjectCache {
                return $value;
        }
 
+       /**
+        * @see BagOStuff::makeKey()
+        * @param string ... Key component
+        * @return string
+        * @since 1.27
+        */
+       public function makeKey() {
+               return call_user_func_array( array( $this->cache, __FUNCTION__ ), func_get_args() );
+       }
+
+       /**
+        * @see BagOStuff::makeGlobalKey()
+        * @param string ... Key component
+        * @return string
+        * @since 1.27
+        */
+       public function makeGlobalKey() {
+               return call_user_func_array( array( $this->cache, __FUNCTION__ ), func_get_args() );
+       }
+
        /**
         * Get the "last error" registered; clearLastError() should be called manually
         * @return int ERR_* constant for the "last error" registry
@@ -888,7 +931,7 @@ class WANObjectCache {
         *
         * @param mixed $value
         * @param integer $ttl [0=forever]
-        * @return string
+        * @return array
         */
        protected function wrap( $value, $ttl ) {
                return array(
@@ -911,7 +954,7 @@ class WANObjectCache {
                $purgeTimestamp = self::parsePurgeValue( $wrapped );
                if ( is_float( $purgeTimestamp ) ) {
                        // Purged values should always have a negative current $ttl
-                       $curTTL = min( -0.000001, $purgeTimestamp - $now );
+                       $curTTL = min( $purgeTimestamp - $now, self::TINY_NEGATIVE );
                        return array( false, $curTTL );
                }
 
@@ -922,7 +965,12 @@ class WANObjectCache {
                        return array( false, null );
                }
 
-               if ( $wrapped[self::FLD_TTL] > 0 ) {
+               $flags = isset( $wrapped[self::FLD_FLAGS] ) ? $wrapped[self::FLD_FLAGS] : 0;
+               if ( ( $flags & self::FLG_STALE ) == self::FLG_STALE ) {
+                       // Treat as expired, with the cache time as the expiration
+                       $age = $now - $wrapped[self::FLD_TIME];
+                       $curTTL = min( -$age, self::TINY_NEGATIVE );
+               } elseif ( $wrapped[self::FLD_TTL] > 0 ) {
                        // Get the approximate time left on the key
                        $age = $now - $wrapped[self::FLD_TIME];
                        $curTTL = max( $wrapped[self::FLD_TTL] - $age, 0.0 );