Merge "Make getCacheSetOptions() and WAN cache handle broken replication"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 3 Dec 2015 12:24:37 +0000 (12:24 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 3 Dec 2015 12:24:37 +0000 (12:24 +0000)
1  2 
includes/libs/objectcache/WANObjectCache.php

@@@ -115,7 -115,6 +115,7 @@@ class WANObjectCache implements IExpiri
        const FLD_TTL = 2;
        const FLD_TIME = 3;
        const FLD_FLAGS = 4;
 +      const FLD_HOLDOFF = 5;
  
        /** @var integer Treat this value as expired-on-arrival */
        const FLG_STALE = 1;
                $vPrefixLen = strlen( self::VALUE_KEY_PREFIX );
                $valueKeys = self::prefixCacheKeys( $keys, self::VALUE_KEY_PREFIX );
  
 -              $checksForAll = array();
 -              $checksByKey = array();
 +              $checkKeysForAll = array();
 +              $checkKeysByKey = array();
                $checkKeysFlat = array();
                foreach ( $checkKeys as $i => $keys ) {
                        $prefixed = self::prefixCacheKeys( (array)$keys, self::TIME_KEY_PREFIX );
                        $checkKeysFlat = array_merge( $checkKeysFlat, $prefixed );
                        // Is this check keys for a specific cache key, or for all keys being fetched?
                        if ( is_int( $i ) ) {
 -                              $checksForAll = array_merge( $checksForAll, $prefixed );
 +                              $checkKeysForAll = array_merge( $checkKeysForAll, $prefixed );
                        } else {
 -                              $checksByKey[$i] = isset( $checksByKey[$i] )
 -                                      ? array_merge( $checksByKey[$i], $prefixed )
 +                              $checkKeysByKey[$i] = isset( $checkKeysByKey[$i] )
 +                                      ? array_merge( $checkKeysByKey[$i], $prefixed )
                                        : $prefixed;
                        }
                }
                $now = microtime( true );
  
                // Collect timestamps from all "check" keys
 -              $checkKeyTimesForAll = $this->processCheckKeys( $checksForAll, $wrappedValues, $now );
 -              $checkKeyTimesByKey = array();
 -              foreach ( $checksByKey as $cacheKey => $checks ) {
 -                      $checkKeyTimesByKey[$cacheKey] =
 +              $purgeValuesForAll = $this->processCheckKeys( $checkKeysForAll, $wrappedValues, $now );
 +              $purgeValuesByKey = array();
 +              foreach ( $checkKeysByKey as $cacheKey => $checks ) {
 +                      $purgeValuesByKey[$cacheKey] =
                                $this->processCheckKeys( $checks, $wrappedValues, $now );
                }
  
  
                                // Force dependant keys to be invalid for a while after purging
                                // to reduce race conditions involving stale data getting cached
 -                              $checkKeyTimes = $checkKeyTimesForAll;
 -                              if ( isset( $checkKeyTimesByKey[$key] ) ) {
 -                                      $checkKeyTimes = array_merge( $checkKeyTimes, $checkKeyTimesByKey[$key] );
 +                              $purgeValues = $purgeValuesForAll;
 +                              if ( isset( $purgeValuesByKey[$key] ) ) {
 +                                      $purgeValues = array_merge( $purgeValues, $purgeValuesByKey[$key] );
                                }
 -                              foreach ( $checkKeyTimes as $checkKeyTime ) {
 -                                      $safeTimestamp = $checkKeyTime + self::HOLDOFF_TTL;
 +                              foreach ( $purgeValues as $purge ) {
 +                                      $safeTimestamp = $purge[self::FLD_TIME] + $purge[self::FLD_HOLDOFF];
                                        if ( $safeTimestamp >= $wrappedValues[$vKey][self::FLD_TIME] ) {
 -                                              $curTTL = min( $curTTL, $checkKeyTime - $now );
 +                                              $curTTL = min( $curTTL, $purge[self::FLD_TIME] - $now );
                                        }
                                }
                        }
         * @param array $timeKeys List of prefixed time check keys
         * @param array $wrappedValues
         * @param float $now
 -       * @return array List of timestamps
 +       * @return array List of purge value arrays
         */
        private function processCheckKeys( array $timeKeys, array $wrappedValues, $now ) {
 -              $times = array();
 +              $purgeValues = array();
                foreach ( $timeKeys as $timeKey ) {
 -                      $timestamp = isset( $wrappedValues[$timeKey] )
 +                      $purge = isset( $wrappedValues[$timeKey] )
                                ? self::parsePurgeValue( $wrappedValues[$timeKey] )
                                : false;
 -                      if ( !is_float( $timestamp ) ) {
 +                      if ( $purge === false ) {
                                // Key is not set or invalid; regenerate
 -                              $this->cache->add( $timeKey, self::PURGE_VAL_PREFIX . $now, self::CHECK_KEY_TTL );
 -                              $timestamp = $now;
 +                              $this->cache->add( $timeKey,
 +                                      $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
 +                                      self::CHECK_KEY_TTL
 +                              );
 +                              $purge = array( self::FLD_TIME => $now, self::FLD_HOLDOFF => self::HOLDOFF_TTL );
                        }
 -                      $times[] = $timestamp;
 +                      $purgeValues[] = $purge;
                }
 -              return $times;
 +              return $purgeValues;
        }
  
        /**
  
                $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 ) {
+               if ( $lag === false || ( $lag + $age ) > self::MAX_READ_LAG ) {
                        // Case A: read lag with "lockTSE"; save but record value as stale
                        if ( $lockTSE >= 0 ) {
                                $ttl = max( 1, (int)$lockTSE ); // set() expects seconds
  
                                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 ) {
+                       } elseif ( $lag === false || $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()
                        $ok = $this->relayDelete( $key ) && $ok;
                } else {
                        // Update the local datacenter immediately
 -                      $ok = $this->cache->set( $key, self::PURGE_VAL_PREFIX . microtime( true ), $ttl );
 +                      $ok = $this->cache->set( $key,
 +                              $this->makePurgeValue( microtime( true ), self::HOLDOFF_NONE ),
 +                              $ttl
 +                      );
                        // Publish the purge to all datacenters
 -                      $ok = $this->relayPurge( $key, $ttl ) && $ok;
 +                      $ok = $this->relayPurge( $key, $ttl, self::HOLDOFF_NONE ) && $ok;
                }
  
                return $ok;
         * Note that "check" keys won't collide with other regular keys.
         *
         * @param string $key
 -       * @return float UNIX timestamp of the key
 +       * @return float UNIX timestamp of the check key
         */
        final public function getCheckKeyTime( $key ) {
                $key = self::TIME_KEY_PREFIX . $key;
  
 -              $time = self::parsePurgeValue( $this->cache->get( $key ) );
 -              if ( $time === false ) {
 +              $purge = self::parsePurgeValue( $this->cache->get( $key ) );
 +              if ( $purge !== false ) {
 +                      $time = $purge[self::FLD_TIME];
 +              } else {
                        // Casting assures identical floats for the next getCheckKeyTime() calls
 -                      $time = (string)microtime( true );
 -                      $this->cache->add( $key, self::PURGE_VAL_PREFIX . $time, self::CHECK_KEY_TTL );
 -                      $time = (float)$time;
 +                      $now = (string)microtime( true );
 +                      $this->cache->add( $key,
 +                              $this->makePurgeValue( $now, self::HOLDOFF_TTL ),
 +                              self::CHECK_KEY_TTL
 +                      );
 +                      $time = (float)$now;
                }
  
                return $time;
         * @see WANObjectCache::resetCheckKey()
         *
         * @param string $key Cache key
 +       * @param int $holdoff HOLDOFF_TTL or HOLDOFF_NONE constant
         * @return bool True if the item was purged or not found, false on failure
         */
 -      final public function touchCheckKey( $key ) {
 +      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,
 -                      self::PURGE_VAL_PREFIX . microtime( true ), self::CHECK_KEY_TTL );
 +                      $this->makePurgeValue( microtime( true ), $holdoff ),
 +                      self::CHECK_KEY_TTL
 +              );
                // Publish the purge to all datacenters
 -              return $this->relayPurge( $key, self::CHECK_KEY_TTL ) && $ok;
 +              return $this->relayPurge( $key, self::CHECK_KEY_TTL, $holdoff ) && $ok;
        }
  
        /**
        /**
         * Do the actual async bus purge of a key
         *
 -       * This must set the key to "PURGED:<UNIX timestamp>"
 +       * This must set the key to "PURGED:<UNIX timestamp>:<holdoff>"
         *
         * @param string $key Cache key
         * @param integer $ttl How long to keep the tombstone [seconds]
 +       * @param integer $holdoff HOLDOFF_* constant controlling how long to ignore sets for this key
         * @return bool Success
         */
 -      protected function relayPurge( $key, $ttl ) {
 +      protected function relayPurge( $key, $ttl, $holdoff ) {
                $event = $this->cache->modifySimpleRelayEvent( array(
                        'cmd' => 'set',
                        'key' => $key,
 -                      'val' => 'PURGED:$UNIXTIME$',
 +                      'val' => 'PURGED:$UNIXTIME$:' . (int)$holdoff,
                        'ttl' => max( $ttl, 1 ),
                        'sbt' => true, // substitute $UNIXTIME$ with actual microtime
                ) );
         */
        protected function unwrap( $wrapped, $now ) {
                // Check if the value is a tombstone
 -              $purgeTimestamp = self::parsePurgeValue( $wrapped );
 -              if ( is_float( $purgeTimestamp ) ) {
 +              $purge = self::parsePurgeValue( $wrapped );
 +              if ( $purge !== false ) {
                        // Purged values should always have a negative current $ttl
 -                      $curTTL = min( $purgeTimestamp - $now, self::TINY_NEGATIVE );
 +                      $curTTL = min( $purge[self::FLD_TIME] - $now, self::TINY_NEGATIVE );
                        return array( false, $curTTL );
                }
  
        }
  
        /**
 -       * @param string $value String like "PURGED:<timestamp>"
 -       * @return float|bool UNIX timestamp or false on failure
 +       * @param string $value Wrapped value like "PURGED:<timestamp>:<holdoff>"
 +       * @return array|bool Array containing a UNIX timestamp (float) and holdoff period (integer),
 +       *  or false if value isn't a valid purge value
         */
        protected static function parsePurgeValue( $value ) {
 -              $m = array();
 -              if ( is_string( $value ) &&
 -                      preg_match( '/^' . self::PURGE_VAL_PREFIX . '([^:]+)$/', $value, $m )
 +              if ( !is_string( $value ) ) {
 +                      return false;
 +              }
 +              $segments = explode( ':', $value, 3 );
 +              if ( !isset( $segments[0] ) || !isset( $segments[1] )
 +                      || "{$segments[0]}:" !== self::PURGE_VAL_PREFIX
                ) {
 -                      return (float)$m[1];
 -              } else {
                        return false;
                }
 +              if ( !isset( $segments[2] ) ) {
 +                      // Back-compat with old purge values without holdoff
 +                      $segments[2] = self::HOLDOFF_TTL;
 +              }
 +              return array(
 +                      self::FLD_TIME => (float)$segments[1],
 +                      self::FLD_HOLDOFF => (int)$segments[2],
 +              );
 +      }
 +
 +      /**
 +       * @param float $timestamp
 +       * @param int $holdoff In seconds
 +       * @return string Wrapped purge value
 +       */
 +      protected static function makePurgeValue( $timestamp, $holdoff ) {
 +              return self::PURGE_VAL_PREFIX . (float)$timestamp . ':' . (int)$holdoff;
        }
  }