Rename DB_SLAVE constant to DB_REPLICA
[lhc/web/wiklou.git] / includes / libs / objectcache / WANObjectCache.php
index 4fd40e2..200ddfa 100644 (file)
@@ -33,6 +33,7 @@ use Psr\Log\NullLogger;
  * This class is intended for caching data from primary stores.
  * If the get() method does not return a value, then the caller
  * should query the new value and backfill the cache using set().
+ * The preferred way to do this logic is through getWithSetCallback().
  * When querying the store on cache miss, the closest DB replica
  * should be used. Try to avoid heavyweight DB master or quorum reads.
  * When the source data changes, a purge method should be called.
@@ -43,16 +44,23 @@ use Psr\Log\NullLogger;
  *
  * The simplest purge method is delete().
  *
- * Instances of this class must be configured to point to a valid
- * PubSub endpoint, and there must be listeners on the cache servers
- * that subscribe to the endpoint and update the caches.
+ * There are two supported ways to handle broadcasted operations:
+ *   - a) Configure the 'purge' EventRelayer to point to a valid PubSub endpoint
+ *        that has subscribed listeners on the cache servers applying the cache updates.
+ *   - b) Ignore the 'purge' EventRelayer configuration (default is NullEventRelayer)
+ *        and set up mcrouter as the underlying cache backend, using one of the memcached
+ *        BagOStuff classes as 'cache'. Use OperationSelectorRoute in the mcrouter settings
+ *        to configure 'set' and 'delete' operations to go to all DCs via AllAsyncRoute and
+ *        configure other operations to go to the local DC via PoolRoute (for reference,
+ *        see https://github.com/facebook/mcrouter/wiki/List-of-Route-Handles).
  *
- * Broadcasted operations like delete() and touchCheckKey() are done
- * synchronously in the local datacenter, but are relayed asynchronously.
- * This means that callers in other datacenters will see older values
- * for however many milliseconds the datacenters are apart. As with
- * any cache, this should not be relied on for cases where reads are
- * used to determine writes to source (e.g. non-cache) data stores.
+ * Broadcasted operations like delete() and touchCheckKey() are done asynchronously
+ * in all datacenters this way, though the local one should likely be near immediate.
+ *
+ * This means that callers in all datacenters may see older values for however many
+ * milliseconds that the purge took to reach that datacenter. As with any cache, this
+ * should not be relied on for cases where reads are used to determine writes to source
+ * (e.g. non-cache) data stores, except when reading immutable data.
  *
  * All values are wrapped in metadata arrays. Keys use a "WANCache:" prefix
  * to avoid collisions with keys that are not wrapped as metadata arrays. The
@@ -60,6 +68,7 @@ use Psr\Log\NullLogger;
  *   - a) "WANCache:v" : used for regular value keys
  *   - b) "WANCache:i" : used for temporarily storing values of tombstoned keys
  *   - c) "WANCache:t" : used for storing timestamp "check" keys
+ *   - d) "WANCache:m" : used for temporary mutex keys to avoid cache stampedes
  *
  * @ingroup Cache
  * @since 1.26
@@ -129,6 +138,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        const VALUE_KEY_PREFIX = 'WANCache:v:';
        const INTERIM_KEY_PREFIX = 'WANCache:i:';
        const TIME_KEY_PREFIX = 'WANCache:t:';
+       const MUTEX_KEY_PREFIX = 'WANCache:m:';
 
        const PURGE_VAL_PREFIX = 'PURGED:';
 
@@ -354,7 +364,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * Example usage:
         * @code
-        *     $dbr = wfGetDB( DB_SLAVE );
+        *     $dbr = wfGetDB( DB_REPLICA );
         *     $setOpts = Database::getCacheSetOptions( $dbr );
         *     // Fetch the row from the DB
         *     $row = $dbr->selectRow( ... );
@@ -367,8 +377,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @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
+        *   - lag     : Seconds of replica DB lag. Typically, this is either the replica DB lag
+        *               before the data was read or, if applicable, the replica DB lag before
         *               the snapshot-isolated transaction the data was read from started.
         *               Default: 0 seconds
         *   - since   : UNIX timestamp of the data in $value. Typically, this is either
@@ -456,8 +466,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * When using potentially long-running ACID transactions, a good pattern is
         * to use a pre-commit hook to issue the delete. This means that immediately
-        * after commit, callers will see the tombstone in cache in the local datacenter
-        * and in the others upon relay. It also avoids the following race condition:
+        * after commit, callers will see the tombstone in cache upon purge relay.
+        * It also avoids the following race condition:
         *   - a) T1 begins, changes a row, and calls delete()
         *   - b) The HOLDOFF_TTL passes, expiring the delete() tombstone
         *   - c) T2 starts, reads the row and calls set() due to a cache miss
@@ -466,7 +476,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * Example usage:
         * @code
-        *     $dbw->begin( __METHOD__ ); // start of request
+        *     $dbw->startAtomic( __METHOD__ ); // start of request
         *     ... <execute some stuff> ...
         *     // Update the row in the DB
         *     $dbw->update( ... );
@@ -476,7 +486,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *         $cache->delete( $key );
         *     } );
         *     ... <execute some stuff> ...
-        *     $dbw->commit( __METHOD__ ); // end of request
+        *     $dbw->endAtomic( __METHOD__ ); // end of request
         * @endcode
         *
         * The $ttl parameter can be used when purging values that have not actually changed
@@ -495,18 +505,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $key = self::VALUE_KEY_PREFIX . $key;
 
                if ( $ttl <= 0 ) {
-                       // Update the local datacenter immediately
-                       $ok = $this->cache->delete( $key );
                        // Publish the purge to all datacenters
-                       $ok = $this->relayDelete( $key ) && $ok;
+                       $ok = $this->relayDelete( $key );
                } else {
-                       // Update the local datacenter immediately
-                       $ok = $this->cache->set( $key,
-                               $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
-                               $ttl
-                       );
                        // Publish the purge to all datacenters
-                       $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
+                       $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE );
                }
 
                return $ok;
@@ -559,17 +562,19 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * keys, the relevant "check" keys must be supplied for this to work.
         *
         * The "check" key essentially represents a last-modified field.
-        * When touched, keys using it via get(), getMulti(), or getWithSetCallback()
-        * will be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
+        * When touched, the field will be updated on all cache servers.
+        * Keys using it via get(), getMulti(), or getWithSetCallback() will
+        * be invalidated. It is treated as being HOLDOFF_TTL seconds in the future
         * by those methods to avoid race conditions where dependent keys get updated
-        * with stale values (e.g. from a DB slave).
+        * with stale values (e.g. from a DB replica DB).
         *
         * This is typically useful for keys with hardcoded names or in some cases
         * dynamically generated names where a low number of combinations exist.
         * When a few important keys get a large number of hits, a high cache
         * time is usually desired as well as "lockTSE" logic. The resetCheckKey()
         * method is less appropriate in such cases since the "time since expiry"
-        * cannot be inferred.
+        * cannot be inferred, causing any get() after the reset to treat the key
+        * as being "hot", resulting in more stale value usage.
         *
         * Note that "check" keys won't collide with other regular keys.
         *
@@ -582,14 +587,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool True if the item was purged or not found, false on failure
         */
        final public function touchCheckKey( $key, $holdoff = self::HOLDOFF_TTL ) {
-               $key = self::TIME_KEY_PREFIX . $key;
-               // Update the local datacenter immediately
-               $ok = $this->cache->set( $key,
-                       $this->makePurgeValue( microtime( true ), $holdoff ),
-                       self::CHECK_KEY_TTL
-               );
                // Publish the purge to all datacenters
-               return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
+               return $this->relayPurge( self::TIME_KEY_PREFIX . $key, self::CHECK_KEY_TTL, $holdoff );
        }
 
        /**
@@ -597,11 +596,14 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *
         * This is similar to touchCheckKey() in that keys using it via get(), getMulti(),
         * or getWithSetCallback() will be invalidated. The differences are:
-        *   - a) The timestamp will be deleted from all caches and lazily
+        *   - a) The "check" key will be deleted from all caches and lazily
         *        re-initialized when accessed (rather than set everywhere)
         *   - b) Thus, dependent keys will be known to be invalid, but not
         *        for how long (they are treated as "just" purged), which
         *        effects any lockTSE logic in getWithSetCallback()
+        *   - c) Since "check" keys are initialized only on the server the key hashes
+        *        to, any temporary ejection of that server will cause the value to be
+        *        seen as purged as a new server will initialize the "check" key.
         *
         * The advantage is that this does not place high TTL keys on every cache
         * server, making it better for code that will cache many different keys
@@ -620,11 +622,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool True if the item was purged or not found, false on failure
         */
        final public function resetCheckKey( $key ) {
-               $key = self::TIME_KEY_PREFIX . $key;
-               // Update the local datacenter immediately
-               $ok = $this->cache->delete( $key );
                // Publish the purge to all datacenters
-               return $this->relayDelete( $key ) && $ok;
+               return $this->relayDelete( self::TIME_KEY_PREFIX . $key );
        }
 
        /**
@@ -661,8 +660,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *         $cache::TTL_MINUTE,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
-        *             $dbr = wfGetDB( DB_SLAVE );
-        *             // Account for any snapshot/slave lag
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
         *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return $dbr->selectRow( ... );
@@ -679,8 +678,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *         $cache::TTL_DAY,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
-        *             $dbr = wfGetDB( DB_SLAVE );
-        *             // Account for any snapshot/slave lag
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
         *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return CatConfig::newFromRow( $dbr->selectRow( ... ) );
@@ -689,7 +688,9 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *             // Calling touchCheckKey() on this key invalidates the cache
         *             'checkKeys' => [ $cache->makeKey( 'site-cat-config' ) ],
         *             // Try to only let one datacenter thread manage cache updates at a time
-        *             'lockTSE' => 30
+        *             'lockTSE' => 30,
+        *             // Avoid querying cache servers multiple times in a web request
+        *             'pcTTL' => $cache::TTL_PROC_LONG
         *         ]
         *     );
         * @endcode
@@ -704,8 +705,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
         *             // Determine new value from the DB
-        *             $dbr = wfGetDB( DB_SLAVE );
-        *             // Account for any snapshot/slave lag
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
         *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             return CatState::newFromResults( $dbr->select( ... ) );
@@ -731,8 +732,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *         10,
         *         // Function that derives the new key value
         *         function ( $oldValue, &$ttl, array &$setOpts ) {
-        *             $dbr = wfGetDB( DB_SLAVE );
-        *             // Account for any snapshot/slave lag
+        *             $dbr = wfGetDB( DB_REPLICA );
+        *             // Account for any snapshot/replica DB lag
         *             $setOpts += Database::getCacheSetOptions( $dbr );
         *
         *             // Start off with the last cached list
@@ -743,8 +744,12 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *             // Merge them and get the new "last 100" rows
         *             return array_slice( array_merge( $new, $list ), 0, 100 );
         *        },
-        *        // Try to only let one datacenter thread manage cache updates at a time
-        *        [ 'lockTSE' => 30 ]
+        *        [
+        *             // Try to only let one datacenter thread manage cache updates at a time
+        *             'lockTSE' => 30,
+        *             // Use a magic value when no cache value is ready rather than stampeding
+        *             'busyValue' => 'computing'
+        *        ]
         *     );
         * @endcode
         *
@@ -779,7 +784,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *   - pcTTL: Process cache the value in this PHP instance for this many seconds. This avoids
         *      network I/O when a key is read several times. This will not cache when the callback
         *      returns false, however. Note that any purges will not be seen while process cached;
-        *      since the callback should use slave DBs and they may be lagged or have snapshot
+        *      since the callback should use replica DBs and they may be lagged or have snapshot
         *      isolation anyway, this should not typically matter.
         *      Default: WANObjectCache::TTL_UNCACHEABLE.
         *   - version: Integer version number. This allows for callers to make breaking changes to
@@ -902,7 +907,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $lockAcquired = false;
                if ( $useMutex ) {
                        // Acquire a datacenter-local non-blocking lock
-                       if ( $this->cache->lock( $key, 0, self::LOCK_TTL ) ) {
+                       if ( $this->cache->add( self::MUTEX_KEY_PREFIX . $key, 1, self::LOCK_TTL ) ) {
                                // Lock acquired; this thread should update the key
                                $lockAcquired = true;
                        } elseif ( $value !== false && $this->isValid( $value, $versioned, $asOf, $minTime ) ) {
@@ -939,11 +944,15 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                if ( ( $isTombstone && $lockTSE > 0 ) && $value !== false && $ttl >= 0 ) {
                        $tempTTL = max( 1, (int)$lockTSE ); // set() expects seconds
                        $wrapped = $this->wrap( $value, $tempTTL, $asOf );
-                       $this->cache->set( self::INTERIM_KEY_PREFIX . $key, $wrapped, $tempTTL );
-               }
-
-               if ( $lockAcquired ) {
-                       $this->cache->unlock( $key );
+                       // Avoid using set() to avoid pointless mcrouter broadcasting
+                       $this->cache->merge(
+                               self::INTERIM_KEY_PREFIX . $key,
+                               function () use ( $wrapped ) {
+                                       return $wrapped;
+                               },
+                               $tempTTL,
+                               1
+                       );
                }
 
                if ( $value !== false && $ttl >= 0 ) {
@@ -952,6 +961,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                        $this->set( $key, $value, $ttl, $setOpts );
                }
 
+               if ( $lockAcquired ) {
+                       // Avoid using delete() to avoid pointless mcrouter broadcasting
+                       $this->cache->changeTTL( self::MUTEX_KEY_PREFIX . $key, 1 );
+               }
+
                return $value;
        }
 
@@ -1028,6 +1042,43 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return $this->cache->getQoS( $flag );
        }
 
+       /**
+        * Get a TTL that is higher for objects that have not changed recently
+        *
+        * This is useful for keys that get explicit purges and DB or purge relay
+        * lag is a potential concern (especially how it interacts with CDN cache)
+        *
+        * Example usage:
+        * @code
+        *     // Last-modified time of page
+        *     $mtime = wfTimestamp( TS_UNIX, $page->getTimestamp() );
+        *     // Get adjusted TTL. If $mtime is 3600 seconds ago and $minTTL/$factor left at
+        *     // defaults, then $ttl is 3600 * .2 = 720. If $minTTL was greater than 720, then
+        *     // $ttl would be $minTTL. If $maxTTL was smaller than 720, $ttl would be $maxTTL.
+        *     $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY );
+        * @endcode
+        *
+        * @param integer|float $mtime UNIX timestamp
+        * @param integer $maxTTL Maximum TTL (seconds)
+        * @param integer $minTTL Minimum TTL (seconds); Default: 30
+        * @param float $factor Value in the range (0,1); Default: .2
+        * @return integer Adaptive TTL
+        * @since 1.28
+        */
+       public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = .2 ) {
+               if ( is_float( $mtime ) ) {
+                       $mtime = (int)$mtime; // ignore fractional seconds
+               }
+
+               if ( !is_int( $mtime ) || $mtime <= 0 ) {
+                       return $minTTL; // no last-modified time provided
+               }
+
+               $age = time() - $mtime;
+
+               return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
+       }
+
        /**
         * Do the actual async bus purge of a key
         *
@@ -1039,17 +1090,25 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         */
        protected function relayPurge( $key, $ttl, $holdoff ) {
-               $event = $this->cache->modifySimpleRelayEvent( [
-                       'cmd' => 'set',
-                       'key' => $key,
-                       'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
-                       'ttl' => max( $ttl, 1 ),
-                       'sbt' => true, // substitute $UNIXTIME$ with actual microtime
-               ] );
-
-               $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
-               if ( !$ok ) {
-                       $this->lastRelayError = self::ERR_RELAY;
+               if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+                       // This handles the mcrouter and the single-DC case
+                       $ok = $this->cache->set( $key,
+                               $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
+                               $ttl
+                       );
+               } else {
+                       $event = $this->cache->modifySimpleRelayEvent( [
+                               'cmd' => 'set',
+                               'key' => $key,
+                               'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
+                               'ttl' => max( $ttl, 1 ),
+                               'sbt' => true, // substitute $UNIXTIME$ with actual microtime
+                       ] );
+
+                       $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+                       if ( !$ok ) {
+                               $this->lastRelayError = self::ERR_RELAY;
+                       }
                }
 
                return $ok;
@@ -1062,14 +1121,19 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @return bool Success
         */
        protected function relayDelete( $key ) {
-               $event = $this->cache->modifySimpleRelayEvent( [
-                       'cmd' => 'delete',
-                       'key' => $key,
-               ] );
-
-               $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
-               if ( !$ok ) {
-                       $this->lastRelayError = self::ERR_RELAY;
+               if ( $this->purgeRelayer instanceof EventRelayerNull ) {
+                       // This handles the mcrouter and the single-DC case
+                       $ok = $this->cache->delete( $key );
+               } else {
+                       $event = $this->cache->modifySimpleRelayEvent( [
+                               'cmd' => 'delete',
+                               'key' => $key,
+                       ] );
+
+                       $ok = $this->purgeRelayer->notify( $this->purgeChannel, $event );
+                       if ( !$ok ) {
+                               $this->lastRelayError = self::ERR_RELAY;
+                       }
                }
 
                return $ok;