* MediaWiki message cache structure version.
* Bump this whenever the message cache format has changed.
*/
-define( 'MSG_CACHE_VERSION', 1 );
+define( 'MSG_CACHE_VERSION', 2 );
/**
* Memcached timeout when loading a key.
*/
define( 'MSG_LOAD_TIMEOUT', 60 );
-/**
- * Memcached timeout when locking a key for a writing operation.
- * See MessageCache::lock()
- */
-define( 'MSG_LOCK_TIMEOUT', 30 );
-/**
- * Number of times we will try to acquire a lock from Memcached.
- * This comes in addition to MSG_LOCK_TIMEOUT.
- */
-define( 'MSG_WAIT_TIMEOUT', 30 );
-
/**
* 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 LOCK_TTL = 30;
+
/**
* Process local cache of loaded messages that are defined in
* MediaWiki namespace. First array level is a language code,
* second level is message key and the values are either message
* content prefixed with space, or !NONEXISTENT for negative
* caching.
+ * @var array $mCache
*/
protected $mCache;
* @return string Normalized message key
*/
public static function normalizeKey( $key ) {
+ global $wgContLang;
$lckey = strtr( $key, ' ', '_' );
if ( ord( $lckey ) < 128 ) {
$lckey[0] = strtolower( $lckey[0] );
* @param int $expiry Lifetime for cache. @see $mExpiry.
*/
function __construct( $memCached, $useDB, $expiry ) {
+ global $wgUseLocalMessageCache;
+
if ( !$memCached ) {
$memCached = wfGetCache( CACHE_NONE );
}
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
+ if ( $wgUseLocalMessageCache ) {
+ $this->localCache = ObjectCache::newAccelerator( CACHE_NONE );
+ } else {
+ $this->localCache = wfGetCache( CACHE_NONE );
+ }
+
$this->wanCache = ObjectCache::getMainWANInstance();
}
}
/**
- * Try to load the cache from a local file.
+ * Try to load the cache from APC.
*
- * @param string $hash The hash of contents, to check validity.
* @param string $code Optional language code, see documenation of load().
- * @return array The cache array
+ * @return array|bool The cache array, or false if not in cache.
*/
- function getLocalCache( $hash, $code ) {
- global $wgCacheDirectory;
-
- $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
-
- # Check file existence
- MediaWiki\suppressWarnings();
- $file = fopen( $filename, 'r' );
- MediaWiki\restoreWarnings();
- if ( !$file ) {
- return false; // No cache file
- }
-
- // Check to see if the file has the hash specified
- $localHash = fread( $file, 32 );
- if ( $hash === $localHash ) {
- // All good, get the rest of it
- $serialized = '';
- while ( !feof( $file ) ) {
- $serialized .= fread( $file, 100000 );
- }
- fclose( $file );
-
- return unserialize( $serialized );
- } else {
- fclose( $file );
-
- return false; // Wrong hash
- }
+ protected function getLocalCache( $code ) {
+ $cacheKey = wfMemcKey( __CLASS__, $code );
+ return $this->localCache->get( $cacheKey );
}
/**
- * Save the cache to a local file.
- * @param string $serialized
- * @param string $hash
+ * Save the cache to APC.
+ *
* @param string $code
+ * @param array $cache The cache array
*/
- function saveToLocal( $serialized, $hash, $code ) {
- global $wgCacheDirectory;
-
- $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
- wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail
-
- MediaWiki\suppressWarnings();
- $file = fopen( $filename, 'w' );
- MediaWiki\restoreWarnings();
-
- if ( !$file ) {
- wfDebug( "Unable to open local cache file for writing\n" );
-
- return;
- }
-
- fwrite( $file, $hash . $serialized );
- fclose( $file );
- MediaWiki\suppressWarnings();
- chmod( $filename, 0666 );
- MediaWiki\restoreWarnings();
+ protected function saveToLocalCache( $code, $cache ) {
+ $cacheKey = wfMemcKey( __CLASS__, $code );
+ $this->localCache->set( $cacheKey, $cache );
}
/**
# Loading code starts
$success = false; # Keep track of success
$staleCache = false; # a cache array with expired data, or false if none has been loaded
- $hashExpired = false; # whether the cluster-local validation hash is stale
$where = array(); # Debug info, delayed to avoid spamming debug log too much
- # Local cache
- # Hash of the contents is stored in memcache, to detect if local cache goes
- # out of date (e.g. due to replace() on some other server)
- if ( $wgUseLocalMessageCache ) {
- list( $hash, $hashExpired ) = $this->getValidationHash( $code );
- if ( $hash ) {
- $cache = $this->getLocalCache( $hash, $code );
- if ( !$cache ) {
- $where[] = 'local cache is empty or has the wrong hash';
- } elseif ( $this->isCacheExpired( $cache ) ) {
- $where[] = 'local cache is expired';
- $staleCache = $cache;
- } elseif ( $hashExpired ) {
- $where[] = 'local cache validation key is expired';
- $staleCache = $cache;
- } else {
- $where[] = 'got from local cache';
- $success = true;
- $this->mCache[$code] = $cache;
- }
+ # Hash of the contents is stored in memcache, to detect if data-center cache
+ # or local cache goes out of date (e.g. due to replace() on some other server)
+ list( $hash, $hashVolatile ) = $this->getValidationHash( $code );
+
+ if ( $wgUseLocalMessageCache && $hash ) {
+ # Try the local cache and check against the cluster hash key...
+ $cache = $this->getLocalCache( $code );
+ if ( !$cache ) {
+ $where[] = 'local cache is empty';
+ } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) {
+ $where[] = 'local cache has the wrong hash';
+ $staleCache = $cache;
+ } elseif ( $this->isCacheExpired( $cache ) ) {
+ $where[] = 'local cache is expired';
+ $staleCache = $cache;
+ } elseif ( $hashVolatile ) {
+ $where[] = 'local cache validation key is expired/volatile';
+ $staleCache = $cache;
+ } else {
+ $where[] = 'got from local cache';
+ $success = true;
+ $this->mCache[$code] = $cache;
}
}
# 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++ ) {
- if ( $hashExpired && $staleCache ) {
+ 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
# below, and use the local stale value if it was not acquired.
} elseif ( $this->isCacheExpired( $cache ) ) {
$where[] = 'global cache is expired';
$staleCache = $cache;
+ } elseif ( $hashVolatile ) {
+ # DB results are slave lag prone until the holdoff TTL passes.
+ # By then, updates should be reflected in loadFromDBWithLock().
+ # One thread renerates the cache while others use old values.
+ $where[] = 'global cache is expired/volatile';
+ $staleCache = $cache;
} else {
$where[] = 'got from global cache';
$this->mCache[$code] = $cache;
# 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 ) ) {
- # Load from DB complete, no need to retry
$success = true;
break;
} elseif ( $staleCache ) {
# Disable cache
break;
} else {
- # Wait for the other thread to finish, then retry
+ # 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->lock( $cacheKey );
- $this->unlock( $cacheKey );
+ $this->getReentrantScopedLock( $cacheKey );
}
}
}
$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.
$statusKey = wfMemcKey( 'messages', $code, 'status' );
if ( !$memCache->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ) ) {
return false; // could not acquire lock
# 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.
- if ( $this->lock( $cacheKey ) ) {
- $that = $this;
- $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) {
- $that->unlock( $cacheKey );
- } );
- } else {
- $mainUnlocker = null;
+ $mainUnlocker = $this->getReentrantScopedLock( $cacheKey );
+ if ( !$mainUnlocker ) {
$where[] = 'could not acquire main lock';
}
}
$cache['VERSION'] = MSG_CACHE_VERSION;
+ ksort( $cache );
+ $cache['HASH'] = md5( serialize( $cache ) );
$cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
return $cache;
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().
$cacheKey = wfMemcKey( 'messages', $code );
- $this->lock( $cacheKey );
+ $scopedLock = $this->getReentrantScopedLock( $cacheKey );
$this->load( $code, self::FOR_UPDATE );
$titleKey = wfMemcKey( 'messages', 'individual', $title );
# Update caches
$this->saveToCaches( $this->mCache[$code], 'all', $code );
- $this->unlock( $cacheKey );
+ ScopedCallback::consume( $scopedLock );
$this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) );
// Also delete cached sidebar... just in case it is affected
$success = true;
}
+ $this->setValidationHash( $code, $cache['HASH'] );
+
# Save to local cache
if ( $wgUseLocalMessageCache ) {
- $serialized = serialize( $cache );
- $hash = md5( $serialized );
- $this->setValidationHash( $code, $hash );
- $this->saveToLocal( $serialized, $hash, $code );
+ $this->saveToLocalCache( $code, $cache );
}
return $success;
* Get the md5 used to validate the local disk cache
*
* @param string $code
- * @return array (hash or false, bool expiry status)
+ * @return array (hash or false, bool expiry/volatility status)
*/
protected function getValidationHash( $code ) {
$curTTL = null;
}
/**
- * Represents a write lock on the messages key.
- *
- * Will retry MessageCache::MSG_WAIT_TIMEOUT times, each operations having
- * a timeout of MessageCache::MSG_LOCK_TIMEOUT.
- *
- * @param string $key
- * @return bool Success
+ * @param string $key A language message cache key that stores blobs
+ * @return null|ScopedCallback
*/
- function lock( $key ) {
- $lockKey = $key . ':lock';
- $acquired = false;
- $testDone = false;
- for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) {
- $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT );
- if ( $acquired ) {
- break;
- }
-
- # Fail fast if memcached is totally down
- if ( !$testDone ) {
- $testDone = true;
- if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) {
- break;
- }
- }
- sleep( 1 );
- }
-
- return $acquired;
- }
-
- function unlock( $key ) {
- $lockKey = $key . ':lock';
- $this->mMemc->delete( $lockKey );
+ protected function getReentrantScopedLock( $key ) {
+ return $this->mMemc->getScopedLock( $key, self::WAIT_SEC, self::LOCK_TTL, __METHOD__ );
}
/**