Merge "objectcache: add "hotTTR" and "ageNew" options to getWithSetCallback()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 7 Sep 2016 07:04:59 +0000 (07:04 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 7 Sep 2016 07:04:59 +0000 (07:04 +0000)
1  2 
includes/libs/objectcache/WANObjectCache.php

@@@ -104,6 -104,15 +104,15 @@@ class WANObjectCache implements IExpiri
        /** Default time-since-expiry on a miss that makes a key "hot" */
        const LOCK_TSE = 1;
  
+       /** Never consider performing "popularity" refreshes until a key reaches this age */
+       const AGE_NEW = 60;
+       /** The time length of the "popularity" refresh window for hot keys */
+       const HOT_TTR = 900;
+       /** Hits/second for a refresh to be expected within the "popularity" window */
+       const HIT_RATE_HIGH = 1;
+       /** Seconds to ramp up to the "popularity" refresh chance after a key is no longer new */
+       const RAMPUP_TTL = 30;
        /** Idiom for getWithSetCallback() callbacks to avoid calling set() */
        const TTL_UNCACHEABLE = -1;
        /** Idiom for getWithSetCallback() callbacks to 'lockTSE' logic */
         *
         * Example usage:
         * @code
 -       *     $dbr = wfGetDB( DB_SLAVE );
 +       *     $dbr = wfGetDB( DB_REPLICA );
         *     $setOpts = Database::getCacheSetOptions( $dbr );
         *     // Fetch the row from the DB
         *     $row = $dbr->selectRow( ... );
         * @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
         * 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.
         *         $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( ... );
         *         $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( ... ) );
         *         // 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( ... ) );
         *         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
         *   - checkKeys: List of "check" keys. The key at $key will be seen as invalid when either
         *      touchCheckKey() or resetCheckKey() is called on any of these keys.
         *      Default: [].
-        *   - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less
-        *      than this. It becomes more likely over time, becoming certain once the key is expired.
-        *      Default: WANObjectCache::LOW_TTL.
         *   - lockTSE: If the key is tombstoned or expired (by checkKeys) less than this many seconds
         *      ago, then try to have a single thread handle cache regeneration at any given time.
         *      Other threads will try to use stale values if possible. If, on miss, the time since
         *   - 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
         *      versions are stored alongside older versions concurrently. Avoid storing class objects
         *      however, as this reduces compatibility (due to serialization).
         *      Default: null.
+        *   - hotTTR: Expected time-till-refresh for keys that average ~1 hit/second.
+        *      This should be greater than "ageNew". Keys with higher hit rates will regenerate
+        *      more often. This is useful when a popular key is changed but the cache purge was
+        *      delayed or lost. Seldom used keys are rarely affected by this setting, unless an
+        *      extremely low "hotTTR" value is passed in.
+        *      Default: WANObjectCache::HOT_TTR.
+        *   - lowTTL: Consider pre-emptive updates when the current TTL (seconds) of the key is less
+        *      than this. It becomes more likely over time, becoming certain once the key is expired.
+        *      Default: WANObjectCache::LOW_TTL.
+        *   - ageNew: Consider popularity refreshes only once a key reaches this age in seconds.
+        *      Default: WANObjectCache::AGE_NEW.
         * @return mixed Value found or written to the key
         * @note Callable type hints are not used to avoid class-autoloading
         */
                $lockTSE = isset( $opts['lockTSE'] ) ? $opts['lockTSE'] : self::TSE_NONE;
                $checkKeys = isset( $opts['checkKeys'] ) ? $opts['checkKeys'] : [];
                $busyValue = isset( $opts['busyValue'] ) ? $opts['busyValue'] : null;
+               $popWindow = isset( $opts['hotTTR'] ) ? $opts['hotTTR'] : self::HOT_TTR;
+               $ageNew = isset( $opts['ageNew'] ) ? $opts['ageNew'] : self::AGE_NEW;
                $minTime = isset( $opts['minTime'] ) ? $opts['minTime'] : 0.0;
                $versioned = isset( $opts['version'] );
  
                if ( $value !== false
                        && $curTTL > 0
                        && $this->isValid( $value, $versioned, $asOf, $minTime )
-                       && !$this->worthRefresh( $curTTL, $lowTTL )
+                       && !$this->worthRefreshExpiring( $curTTL, $lowTTL )
+                       && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow )
                ) {
                        return $value;
                }
         * @param float $lowTTL Consider a refresh when $curTTL is less than this
         * @return bool
         */
-       protected function worthRefresh( $curTTL, $lowTTL ) {
+       protected function worthRefreshExpiring( $curTTL, $lowTTL ) {
                if ( $curTTL >= $lowTTL ) {
                        return false;
                } elseif ( $curTTL <= 0 ) {
                return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
        }
  
+       /**
+        * Check if a key is due for randomized regeneration due to its popularity
+        *
+        * This is used so that popular keys can preemptively refresh themselves for higher
+        * consistency (especially in the case of purge loss/delay). Unpopular keys can remain
+        * in cache with their high nominal TTL. This means popular keys keep good consistency,
+        * whether the data changes frequently or not, and long-tail keys get to stay in cache
+        * and get hits too. Similar to worthRefreshExpiring(), randomization is used.
+        *
+        * @param float $asOf UNIX timestamp of the value
+        * @param integer $ageNew Age of key when this might recommend refreshing (seconds)
+        * @param integer $timeTillRefresh Age of key when it should be refreshed if popular (seconds)
+        * @return bool
+        */
+       protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh ) {
+               $age = microtime( true ) - $asOf;
+               $timeOld = $age - $ageNew;
+               if ( $timeOld <= 0 ) {
+                       return false;
+               }
+               // Lifecycle is: new, ramp-up refresh chance, full refresh chance
+               $refreshWindowSec = max( $timeTillRefresh - $ageNew - self::RAMPUP_TTL / 2, 1 );
+               // P(refresh) * (# hits in $refreshWindowSec) = (expected # of refreshes)
+               // P(refresh) * ($refreshWindowSec * $popularHitsPerSec) = 1
+               // P(refresh) = 1/($refreshWindowSec * $popularHitsPerSec)
+               $chance = 1 / ( self::HIT_RATE_HIGH * $refreshWindowSec );
+               // Ramp up $chance from 0 to its nominal value over RAMPUP_TTL seconds to avoid stampedes
+               $chance *= ( $timeOld <= self::RAMPUP_TTL ) ? $timeOld / self::RAMPUP_TTL : 1;
+               return mt_rand( 1, 1e9 ) <= 1e9 * $chance;
+       }
        /**
         * Check whether $value is appropriately versioned and not older than $minTime (if set)
         *