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
+ * Process cache of loaded messages that are defined in MediaWiki namespace
+ *
+ * @var MapCacheLRU Map of (language code => key => " <MESSAGE>" or "!TOO BIG")
*/
- protected $mCache;
+ protected $cache;
/**
* @var bool[] Map of (language code => boolean)
*/
- protected $mCacheVolatile = [];
+ protected $cacheVolatile = [];
/**
* Should mean that database cannot be used, but check
/** @var Parser */
protected $mParser;
- /**
- * Variable for tracking which variables are already loaded
- * @var array $mLoadedLanguages
- */
- protected $mLoadedLanguages = [];
-
/**
* @var bool $mInParser
*/
$this->clusterCache = $clusterCache;
$this->srvCache = $serverCache;
+ $this->cache = new MapCacheLRU( 5 ); // limit size for sanity
+
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
}
* is disabled.
*
* @param string $code Language to which load messages
- * @param int $mode Use MessageCache::FOR_UPDATE to skip process cache [optional]
- * @throws MWException
+ * @param int|null $mode Use MessageCache::FOR_UPDATE to skip process cache [optional]
+ * @throws InvalidArgumentException
* @return bool
*/
protected function load( $code, $mode = null ) {
}
# Don't do double loading...
- if ( isset( $this->mLoadedLanguages[$code] ) && $mode != self::FOR_UPDATE ) {
+ if ( $this->cache->has( $code ) && $mode != self::FOR_UPDATE ) {
return true;
}
# 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 );
- $this->mCacheVolatile[$code] = $hashVolatile;
+ $this->cacheVolatile[$code] = $hashVolatile;
# Try the local cache and check against the cluster hash key...
$cache = $this->getLocalCache( $code );
$staleCache = $cache;
} else {
$where[] = 'got from local cache';
+ $this->cache->set( $code, $cache );
$success = true;
- $this->mCache[$code] = $cache;
}
if ( !$success ) {
$staleCache = $cache;
} else {
$where[] = 'got from global cache';
- $this->mCache[$code] = $cache;
+ $this->cache->set( $code, $cache );
$this->saveToCaches( $cache, 'local-only', $code );
$success = true;
}
} elseif ( $staleCache ) {
# Use the stale cache while some other thread constructs the new one
$where[] = 'using stale cache';
- $this->mCache[$code] = $staleCache;
+ $this->cache->set( $code, $staleCache );
$success = true;
break;
} elseif ( $failedAttempts > 0 ) {
if ( !$success ) {
$where[] = 'loading FAILED - cache is disabled';
$this->mDisable = true;
- $this->mCache = false;
+ $this->cache->set( $code, null );
wfDebugLog( 'MessageCacheError', __METHOD__ . ": Failed to load $code\n" );
# This used to throw an exception, but that led to nasty side effects like
# the whole wiki being instantly down if the memcached server died
- } else {
- # All good, just record the success
- $this->mLoadedLanguages[$code] = true;
+ }
+
+ if ( !$this->cache->has( $code ) ) { // sanity
+ throw new LogicException( "Process cache for '$code' should be set by now." );
}
$info = implode( ', ', $where );
/**
* @param string $code
* @param array &$where List of wfDebug() comments
- * @param int $mode Use MessageCache::FOR_UPDATE to use DB_MASTER
+ * @param int|null $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, $mode = null ) {
}
$cache = $this->loadFromDB( $code, $mode );
- $this->mCache[$code] = $cache;
+ $this->cache->set( $code, $cache );
$saveSuccess = $this->saveToCaches( $cache, 'all', $code );
if ( !$saveSuccess ) {
* on-demand from the database later.
*
* @param string $code Language code
- * @param int $mode Use MessageCache::FOR_UPDATE to skip process cache
+ * @param int|null $mode Use MessageCache::FOR_UPDATE to skip process cache
* @return array Loaded messages for storing in caches
*/
protected function loadFromDB( $code, $mode = null ) {
$mostused = [];
if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
- if ( !isset( $this->mCache[$wgLanguageCode] ) ) {
+ if ( !$this->cache->has( $wgLanguageCode ) ) {
$this->load( $wgLanguageCode );
}
- $mostused = array_keys( $this->mCache[$wgLanguageCode] );
+ $mostused = array_keys( $this->cache->get( $wgLanguageCode ) );
foreach ( $mostused as $key => $value ) {
$mostused[$key] = "$value/$code";
}
// (a) Update the process cache with the new message text
if ( $text === false ) {
// Page deleted
- $this->mCache[$code][$title] = '!NONEXISTENT';
+ $this->cache->setField( $code, $title, '!NONEXISTENT' );
} else {
// Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date
- $this->mCache[$code][$title] = ' ' . $text;
+ $this->cache->setField( $code, $title, ' ' . $text );
}
// (b) Update the shared caches in a deferred update with a fresh DB snapshot
[ 'title' => $title, 'code' => $code ] );
return;
}
- // Load the messages from the master DB to avoid race conditions
+ // Reload messages from the database and pre-populate dc-local caches
+ // as optimisation. Use the master DB to avoid race conditions.
$cache = $this->loadFromDB( $code, self::FOR_UPDATE );
- $this->mCache[$code] = $cache;
- // Load the process cache values and set the per-title cache keys
+ // Check if an individual cache key should exist and update cache accordingly
$page = WikiPage::factory( Title::makeTitle( NS_MEDIAWIKI, $title ) );
$page->loadPageData( $page::READ_LATEST );
$text = $this->getMessageTextFromContent( $page->getContent() );
- // Check if an individual cache key should exist and update cache accordingly
if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- $titleKey = $this->bigMessageCacheKey( $this->mCache[$code]['HASH'], $title );
- $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
+ // Match logic of loadCachedMessagePageEntry()
+ $this->wanCache->set(
+ $this->bigMessageCacheKey( $cache['HASH'], $title ),
+ ' ' . $text,
+ $this->mExpiry
+ );
}
// Mark this cache as definitely being "latest" (non-volatile) so
- // load() calls do try to refresh the cache with replica DB data
- $this->mCache[$code]['LATEST'] = time();
+ // load() calls do not try to refresh the cache with replica DB data
+ $cache['LATEST'] = time();
+ // Update the process cache
+ $this->cache->set( $code, $cache );
// Pre-emptively update the local datacenter cache so things like edit filter and
// blacklist changes are reflected immediately; these often use MediaWiki: pages.
// The datacenter handling replace() calls should be the same one handling edits
// as they require HTTP POST.
- $this->saveToCaches( $this->mCache[$code], 'all', $code );
+ $this->saveToCaches( $cache, 'all', $code );
// Release the lock now that the cache is saved
ScopedCallback::consume( $scopedLock );
* @return string|bool The message, or false if it does not exist or on error
*/
public function getMsgFromNamespace( $title, $code ) {
+ // Load all MediaWiki page definitions into cache. Note that individual keys
+ // already loaded into cache during this request remain in the cache, which
+ // includes the value of hook-defined messages.
$this->load( $code );
- if ( isset( $this->mCache[$code][$title] ) ) {
- $entry = $this->mCache[$code][$title];
+ $entry = $this->cache->getField( $code, $title );
+ if ( $entry !== null ) {
if ( substr( $entry, 0, 1 ) === ' ' ) {
// The message exists and is not '!TOO BIG'
return (string)substr( $entry, 1 );
}
// Fall through and try invididual message cache below
} else {
+ // Message does not have a MediaWiki page definition
$message = false;
Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
if ( $message !== false ) {
- $this->mCache[$code][$title] = ' ' . $message;
+ $this->cache->setField( $code, $title, ' ' . $message );
} else {
- $this->mCache[$code][$title] = '!NONEXISTENT';
+ $this->cache->setField( $code, $title, '!NONEXISTENT' );
}
return $message;
}
- // Individual message cache key
- $titleKey = $this->bigMessageCacheKey( $this->mCache[$code]['HASH'], $title );
-
- if ( $this->mCacheVolatile[$code] ) {
+ if ( $this->cacheVolatile[$code] ) {
$entry = false;
// Make sure that individual keys respect the WAN cache holdoff period too
LoggerFactory::getInstance( 'MessageCache' )->debug(
__METHOD__ . ': loading volatile key \'{titleKey}\'',
- [ 'titleKey' => $titleKey, 'code' => $code ] );
+ [ 'titleKey' => $title, 'code' => $code ] );
} else {
// Try the individual message cache
- $entry = $this->wanCache->get( $titleKey );
- }
-
- if ( $entry !== false ) {
- if ( substr( $entry, 0, 1 ) === ' ' ) {
- $this->mCache[$code][$title] = $entry;
- // The message exists, so make sure a string is returned
- return (string)substr( $entry, 1 );
- } elseif ( $entry === '!NONEXISTENT' ) {
- $this->mCache[$code][$title] = '!NONEXISTENT';
-
- return false;
- } else {
- // Corrupt/obsolete entry, delete it
- $this->wanCache->delete( $titleKey );
- }
- }
-
- // Try loading the message from the database
- $dbr = wfGetDB( DB_REPLICA );
- $cacheOpts = Database::getCacheSetOptions( $dbr );
- // Use newKnownCurrent() to avoid querying revision/user tables
- $titleObj = Title::makeTitle( NS_MEDIAWIKI, $title );
- if ( $titleObj->getLatestRevID() ) {
- $revision = Revision::newKnownCurrent(
- $dbr,
- $titleObj
+ $entry = $this->loadCachedMessagePageEntry(
+ $title,
+ $code,
+ $this->cache->getField( $code, 'HASH' )
);
- } else {
- $revision = false;
}
- if ( $revision ) {
- $content = $revision->getContent();
- if ( $content ) {
- $message = $this->getMessageTextFromContent( $content );
- if ( is_string( $message ) ) {
- $this->mCache[$code][$title] = ' ' . $message;
- $this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry, $cacheOpts );
- }
- } else {
- // A possibly temporary loading failure
- LoggerFactory::getInstance( 'MessageCache' )->warning(
- __METHOD__ . ': failed to load message page text for \'{titleKey}\'',
- [ 'titleKey' => $titleKey, 'code' => $code ] );
- $message = null; // no negative caching
- }
- } else {
- $message = false; // negative caching
+ if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
+ $this->cache->setField( $code, $title, $entry );
+ // The message exists, so make sure a string is returned
+ return (string)substr( $entry, 1 );
}
- if ( $message === false ) {
- // Negative caching in case a "too big" message is no longer available (deleted)
- $this->mCache[$code][$title] = '!NONEXISTENT';
- $this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry, $cacheOpts );
- }
+ $this->cache->setField( $code, $title, '!NONEXISTENT' );
- return $message;
+ return false;
+ }
+
+ /**
+ * @param string $dbKey
+ * @param string $code
+ * @param string $hash
+ * @return string Either " <MESSAGE>" or "!NONEXISTANT"
+ */
+ private function loadCachedMessagePageEntry( $dbKey, $code, $hash ) {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
+ IExpiringStore::TTL_MINUTE,
+ function () use ( $code, $dbKey, $hash ) {
+ return $this->wanCache->getWithSetCallback(
+ $this->bigMessageCacheKey( $hash, $dbKey ),
+ $this->mExpiry,
+ function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code ) {
+ // Try loading the message from the database
+ $dbr = wfGetDB( DB_REPLICA );
+ $setOpts += Database::getCacheSetOptions( $dbr );
+ // Use newKnownCurrent() to avoid querying revision/user tables
+ $title = Title::makeTitle( NS_MEDIAWIKI, $dbKey );
+ $revision = Revision::newKnownCurrent( $dbr, $title );
+ if ( !$revision ) {
+ // The wiki doesn't have a local override page. Cache absence with normal TTL.
+ // When overrides are created, self::replace() takes care of the cache.
+ return '!NONEXISTENT';
+ }
+ $content = $revision->getContent();
+ if ( $content ) {
+ $message = $this->getMessageTextFromContent( $content );
+ } else {
+ LoggerFactory::getInstance( 'MessageCache' )->warning(
+ __METHOD__ . ': failed to load page text for \'{titleKey}\'',
+ [ 'titleKey' => $dbKey, 'code' => $code ]
+ );
+ $message = null;
+ }
+
+ if ( !is_string( $message ) ) {
+ // Revision failed to load Content, or Content is incompatible with wikitext.
+ // Possibly a temporary loading failure.
+ $ttl = 5;
+
+ return '!NONEXISTENT';
+ }
+
+ return ' ' . $message;
+ }
+ );
+ }
+ );
}
/**
* @param string $message
* @param bool $interface
- * @param Language $language
- * @param Title $title
+ * @param Language|null $language
+ * @param Title|null $title
* @return string
*/
public function transform( $message, $interface = false, $language = null, $title = null ) {
/**
* @param string $text
- * @param Title $title
+ * @param Title|null $title
* @param bool $linestart Whether or not this is at the start of a line
* @param bool $interface Whether this is an interface message
- * @param Language|string $language Language code
+ * @param Language|string|null $language Language code
* @return ParserOutput|string
*/
public function parse( $text, $title = null, $linestart = true,
*
* Mainly used after a mass rebuild
*/
- function clear() {
+ public function clear() {
$langs = Language::fetchLanguageNames( null, 'mw' );
foreach ( array_keys( $langs ) as $code ) {
$this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
}
-
- $this->mLoadedLanguages = [];
+ $this->cache->clear();
}
/**
global $wgContLang;
$this->load( $code );
- if ( !isset( $this->mCache[$code] ) ) {
+ if ( !$this->cache->has( $code ) ) {
// Apparently load() failed
return null;
}
// Remove administrative keys
- $cache = $this->mCache[$code];
+ $cache = $this->cache->get( $code );
unset( $cache['VERSION'] );
unset( $cache['EXPIRY'] );
unset( $cache['EXCESSIVE'] );