*/
define( 'MSG_CACHE_VERSION', 2 );
-/**
- * Memcached timeout when loading a key.
- * See MessageCache::load()
- */
-define( 'MSG_LOAD_TIMEOUT', 60 );
-
/**
* Message cache
* Performs various MediaWiki namespace-related functions
class MessageCache {
const FOR_UPDATE = 1; // force message reload
- /** How long memcached locks last */
- const WAIT_SEC = 30;
/** How long to wait for memcached locks */
+ const WAIT_SEC = 15;
+ /** How long memcached locks last */
const LOCK_TTL = 30;
/**
# Try the global cache. If it is empty, try to acquire a lock. If
# the lock can't be acquired, wait for the other thread to finish
# and then try the global cache a second time.
- for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) {
+ for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) {
if ( $hashVolatile && $staleCache ) {
# Do not bother fetching the whole cache blob to avoid I/O.
# Instead, just try to get the non-blocking $statusKey lock
# We need to call loadFromDB. Limit the concurrency to one process.
# This prevents the site from going down when the cache expires.
- # Note that the slam-protection lock here is non-blocking.
- if ( $this->loadFromDBWithLock( $code, $where ) ) {
+ # Note that the DB slam protection lock here is non-blocking.
+ $loadStatus = $this->loadFromDBWithLock( $code, $where, $mode );
+ if ( $loadStatus === true ) {
$success = true;
break;
} elseif ( $staleCache ) {
$success = true;
break;
} elseif ( $failedAttempts > 0 ) {
- # Already retried once, still failed, so don't do another lock/unlock cycle
+ # Already blocked once, so avoid another lock/unlock cycle.
# This case will typically be hit if memcached is down, or if
- # loadFromDB() takes longer than MSG_WAIT_TIMEOUT
+ # loadFromDB() takes longer than LOCK_WAIT.
$where[] = "could not acquire status key.";
break;
+ } elseif ( $loadStatus === 'cantacquire' ) {
+ # Wait for the other thread to finish, then retry. Normally,
+ # the memcached get() will then yeild the other thread's result.
+ $where[] = 'waited for other thread to complete';
+ $this->getReentrantScopedLock( $cacheKey );
} else {
- $statusKey = wfMemcKey( 'messages', $code, 'status' );
- $status = $this->mMemc->get( $statusKey );
- if ( $status === 'error' ) {
- # Disable cache
- break;
- } else {
- # Wait for the other thread to finish, then retry. Normally,
- # the memcached get() will then yeild the other thread's result.
- $where[] = 'waited for other thread to complete';
- $this->getReentrantScopedLock( $cacheKey );
- }
+ # Disable cache; $loadStatus is 'disabled'
+ break;
}
}
}
/**
* @param string $code
* @param array $where List of wfDebug() comments
- * @return bool Lock acquired and loadFromDB() called
+ * @param integer $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
+ * @return bool|string True on success or one of ("cantacquire", "disabled")
*/
- protected function loadFromDBWithLock( $code, array &$where ) {
+ protected function loadFromDBWithLock( $code, array &$where, $mode = null ) {
global $wgUseLocalMessageCache;
- $memCache = $this->mMemc;
-
- # Get the non-blocking status key lock. This lets the caller quickly know
- # to use any stale cache lying around. Otherwise, it may do a blocking
- # lock to try to obtain the messages.
+ # If cache updates on all levels fail, give up on message overrides.
+ # This is to avoid easy site outages; see $saveSuccess comments below.
$statusKey = wfMemcKey( 'messages', $code, 'status' );
- if ( !$memCache->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ) ) {
- return false; // could not acquire lock
+ $status = $this->mMemc->get( $statusKey );
+ if ( $status === 'error' ) {
+ $where[] = "could not load; method is still globally disabled";
+ return 'disabled';
}
- # Unlock the status key if there is an exception
- $statusUnlocker = new ScopedCallback( function () use ( $memCache, $statusKey ) {
- $memCache->delete( $statusKey );
- } );
-
# Now let's regenerate
$where[] = 'loading from database';
+ # Lock the cache to prevent conflicting writes.
+ # This lock is non-blocking so stale cache can quickly be used.
+ # Note that load() will call a blocking getReentrantScopedLock()
+ # after this if it really need to wait for any current thread.
$cacheKey = wfMemcKey( 'messages', $code );
- # Lock the cache to prevent conflicting writes
- # If this lock fails, it doesn't really matter, it just means the
- # write is potentially non-atomic, e.g. the results of a replace()
- # may be discarded.
- $mainUnlocker = $this->getReentrantScopedLock( $cacheKey );
- if ( !$mainUnlocker ) {
+ $scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 );
+ if ( !$scopedLock ) {
$where[] = 'could not acquire main lock';
+ return 'cantacquire';
}
- $cache = $this->loadFromDB( $code );
+ $cache = $this->loadFromDB( $code, $mode );
$this->mCache[$code] = $cache;
$saveSuccess = $this->saveToCaches( $cache, 'all', $code );
- # Unlock
- ScopedCallback::consume( $mainUnlocker );
- ScopedCallback::consume( $statusUnlocker );
-
if ( !$saveSuccess ) {
# Cache save has failed.
# There are two main scenarios where this could be a problem:
# overhead on every request, and thus saves the wiki from
# complete downtime under moderate traffic conditions.
if ( !$wgUseLocalMessageCache ) {
- $memCache->set( $statusKey, 'error', 60 * 5 );
+ $this->mMemc->set( $statusKey, 'error', 60 * 5 );
$where[] = 'could not save cache, disabled globally for 5 minutes';
} else {
$where[] = "could not save global cache";
* $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded
* on-demand from the database later.
*
- * @param string $code Language code.
- * @return array Loaded messages for storing in caches.
+ * @param string $code Language code
+ * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache
+ * @return array Loaded messages for storing in caches
*/
- function loadFromDB( $code ) {
+ function loadFromDB( $code, $mode = null ) {
global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache;
- $dbr = wfGetDB( DB_SLAVE );
+ $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE );
+
$cache = array();
# Common conditions
/**
* Updates cache as necessary when message page is changed
*
- * @param string $title Name of the page changed.
+ * @param string|bool $title Name of the page changed (false if deleted)
* @param mixed $text New contents of the page.
*/
public function replace( $title, $text ) {
list( $msg, $code ) = $this->figureMessage( $title );
if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) {
- # Content language overrides do not use the /<code> suffix
+ // Content language overrides do not use the /<code> suffix
return;
}
- # Note that if the cache is volatile, load() may trigger a DB fetch.
- # In that case we reenter/reuse the existing cache key lock to avoid
- # a self-deadlock. This is safe as no reads happen *directly* in this
- # method between getReentrantScopedLock() and load() below. There is
- # no risk of data "changing under our feet" for replace().
+ // Note that if the cache is volatile, load() may trigger a DB fetch.
+ // In that case we reenter/reuse the existing cache key lock to avoid
+ // a self-deadlock. This is safe as no reads happen *directly* in this
+ // method between getReentrantScopedLock() and load() below. There is
+ // no risk of data "changing under our feet" for replace().
$cacheKey = wfMemcKey( 'messages', $code );
$scopedLock = $this->getReentrantScopedLock( $cacheKey );
$this->load( $code, self::FOR_UPDATE );
$titleKey = wfMemcKey( 'messages', 'individual', $title );
-
if ( $text === false ) {
- # Article was deleted
+ // Article was deleted
$this->mCache[$code][$title] = '!NONEXISTENT';
$this->wanCache->delete( $titleKey );
} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- # Check for size
+ // Check for size
$this->mCache[$code][$title] = '!TOO BIG';
$this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
} else {
$this->wanCache->delete( $titleKey );
}
- # Update caches
- $this->saveToCaches( $this->mCache[$code], 'all', $code );
+ // Mark this cache as definitely "latest" (non-volatile) so
+ // load() calls do try to refresh the cache with slave data
+ $this->mCache[$code]['LATEST'] = time();
+
+ // Update caches if the lock was acquired
+ if ( $scopedLock ) {
+ $this->saveToCaches( $this->mCache[$code], 'all', $code );
+ }
+
ScopedCallback::consume( $scopedLock );
+ // Relay the purge to APC and other DCs
$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
// Also delete cached sidebar... just in case it is affected
* @param string|bool $code Language code (default: false)
* @return bool
*/
- protected function saveToCaches( $cache, $dest, $code = false ) {
+ protected function saveToCaches( array $cache, $dest, $code = false ) {
if ( $dest === 'all' ) {
$cacheKey = wfMemcKey( 'messages', $code );
$success = $this->mMemc->set( $cacheKey, $cache );
$success = true;
}
- $this->setValidationHash( $code, $cache['HASH'] );
-
- # Save to local cache
+ $this->setValidationHash( $code, $cache );
$this->saveToLocalCache( $code, $cache );
return $success;
}
/**
- * Get the md5 used to validate the local disk cache
+ * Get the md5 used to validate the local APC cache
*
* @param string $code
* @return array (hash or false, bool expiry/volatility status)
protected function getValidationHash( $code ) {
$curTTL = null;
$value = $this->wanCache->get(
- wfMemcKey( 'messages', $code, 'hash' ),
+ wfMemcKey( 'messages', $code, 'hash', 'v1' ),
$curTTL,
array( wfMemcKey( 'messages', $code ) )
);
- $expired = ( $curTTL === null || $curTTL < 0 );
- return array( $value, $expired );
+ if ( !$value ) {
+ // No hash found at all; cache must regenerate to be safe
+ $expired = true;
+ } elseif ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
+ // Cache was recently updated via replace() and should be up-to-date
+ $expired = false;
+ } else {
+ // See if the "check" key was bumped after the hash was generated
+ $expired = ( $curTTL < 0 );
+ }
+
+ return array( $value['hash'], $expired );
}
/**
* Set the md5 used to validate the local disk cache
*
+ * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not
+ * be treated as "volatile" by getValidationHash() for the next few seconds
+ *
* @param string $code
- * @param string $hash
+ * @param array $cache Cached messages with a version
*/
- protected function setValidationHash( $code, $hash ) {
+ protected function setValidationHash( $code, array $cache ) {
$this->wanCache->set(
- wfMemcKey( 'messages', $code, 'hash' ),
- $hash,
+ wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ array(
+ 'hash' => $cache['HASH'],
+ 'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
+ ),
WANObjectCache::TTL_NONE
);
}
/**
* @param string $key A language message cache key that stores blobs
+ * @param integer $timeout Wait timeout in seconds
* @return null|ScopedCallback
*/
- protected function getReentrantScopedLock( $key ) {
- return $this->mMemc->getScopedLock( $key, self::WAIT_SEC, self::LOCK_TTL, __METHOD__ );
+ protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) {
+ return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ );
}
/**