/**
* @var bool[] Map of (language code => boolean)
*/
- protected $mCacheVolatile = [];
+ protected $cacheVolatile = [];
/**
* Should mean that database cannot be used, but check
protected $clusterCache;
/** @var BagOStuff */
protected $srvCache;
+ /** @var Language */
+ protected $contLang;
/**
* Singleton instance
public static function singleton() {
if ( self::$instance === null ) {
global $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgUseLocalMessageCache;
+ $services = MediaWikiServices::getInstance();
self::$instance = new self(
- MediaWikiServices::getInstance()->getMainWANObjectCache(),
+ $services->getMainWANObjectCache(),
wfGetMessageCacheStorage(),
$wgUseLocalMessageCache
- ? MediaWikiServices::getInstance()->getLocalServerObjectCache()
+ ? $services->getLocalServerObjectCache()
: new EmptyBagOStuff(),
$wgUseDatabaseMessages,
- $wgMsgCacheExpiry
+ $wgMsgCacheExpiry,
+ $services->getContentLanguage()
);
}
* @return string Normalized message key
*/
public static function normalizeKey( $key ) {
- global $wgContLang;
-
$lckey = strtr( $key, ' ', '_' );
if ( ord( $lckey ) < 128 ) {
$lckey[0] = strtolower( $lckey[0] );
} else {
- $lckey = $wgContLang->lcfirst( $lckey );
+ $lckey = MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $lckey );
}
return $lckey;
* @param BagOStuff $serverCache
* @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages)
* @param int $expiry Lifetime for cache. @see $mExpiry.
+ * @param Language|null $contLang Content language of site
*/
public function __construct(
WANObjectCache $wanCache,
BagOStuff $clusterCache,
BagOStuff $serverCache,
$useDB,
- $expiry
+ $expiry,
+ Language $contLang = null
) {
$this->wanCache = $wanCache;
$this->clusterCache = $clusterCache;
$this->mDisable = !$useDB;
$this->mExpiry = $expiry;
+ $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
}
/**
* (2) memcached
* (3) from the database.
*
- * When succesfully loading from (2) or (3), all higher level caches are
+ * When successfully loading from (2) or (3), all higher level caches are
* updated for the newest version.
*
* Nothing is loaded if member variable mDisable is true, either manually
* set by calling code or if message loading fails (is this possible?).
*
- * Returns true if cache is already populated or it was succesfully populated,
+ * Returns true if cache is already populated or it was successfully populated,
* or false if populating empty cache fails. Also returns true if MessageCache
* is disabled.
*
# 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 );
if ( !$success ) {
$where[] = 'loading FAILED - cache is disabled';
$this->mDisable = true;
- $this->cache->set( $code, null );
+ $this->cache->set( $code, [] );
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
// (b) Update the shared caches in a deferred update with a fresh DB snapshot
DeferredUpdates::addCallableUpdate(
function () use ( $title, $msg, $code ) {
- global $wgContLang, $wgMaxMsgCacheEntrySize;
+ global $wgMaxMsgCacheEntrySize;
// Allow one caller at a time to avoid race conditions
$scopedLock = $this->getReentrantScopedLock(
$this->clusterCache->makeKey( 'messages', $code )
[ '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 );
// 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() );
if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
+ // Match logic of loadCachedMessagePageEntry()
$this->wanCache->set(
$this->bigMessageCacheKey( $cache['HASH'], $title ),
' ' . $text,
// Purge the message in the message blob store
$resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
$blobStore = $resourceloader->getMessageBlobStore();
- $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) );
+ $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
Hooks::run( 'MessageCacheReplace', [ $title, $text ] );
},
* @return string|bool The message, or false if not found
*/
protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
- global $wgContLang;
-
$alreadyTried = [];
// First try the requested language.
}
// Now try checking the site language.
- $message = $this->getMessageForLang( $wgContLang, $lckey, $useDB, $alreadyTried );
+ $message = $this->getMessageForLang( $this->contLang, $lckey, $useDB, $alreadyTried );
return $message;
}
* @return string|bool The message, or false if not found
*/
private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
- global $wgContLang;
-
$langcode = $lang->getCode();
// Try checking the database for the requested language
if ( $useDB ) {
- $uckey = $wgContLang->ucfirst( $lckey );
+ $uckey = $this->contLang->ucfirst( $lckey );
if ( !isset( $alreadyTried[$langcode] ) ) {
$message = $this->getMsgFromNamespace(
* @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 ( $this->cache->hasField( $code, $title ) ) {
- $entry = $this->cache->getField( $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 ) {
return $message;
}
- // Individual message cache key
- $bigKey = $this->bigMessageCacheKey( $this->cache->getField( $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' => $bigKey, 'code' => $code ] );
+ [ 'titleKey' => $title, 'code' => $code ] );
} else {
// Try the individual message cache
- $entry = $this->wanCache->get( $bigKey );
- }
-
- if ( $entry !== false ) {
- if ( 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 );
- } elseif ( $entry === '!NONEXISTENT' ) {
- $this->cache->setField( $code, $title, '!NONEXISTENT' );
-
- return false;
- } else {
- // Corrupt/obsolete entry, delete it
- $this->wanCache->delete( $bigKey );
- }
- }
-
- // 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->cache->setField( $code, $title, ' ' . $message );
- $this->wanCache->set( $bigKey, ' ' . $message, $this->mExpiry, $cacheOpts );
- }
- } else {
- // A possibly temporary loading failure
- LoggerFactory::getInstance( 'MessageCache' )->warning(
- __METHOD__ . ': failed to load message page text for \'{titleKey}\'',
- [ 'titleKey' => $bigKey, '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->cache->setField( $code, $title, '!NONEXISTENT' );
- $this->wanCache->set( $bigKey, '!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;
+ }
+ );
+ }
+ );
}
/**
* @return array Array of message keys (strings)
*/
public function getAllMessageKeys( $code ) {
- global $wgContLang;
-
$this->load( $code );
if ( !$this->cache->has( $code ) ) {
// Apparently load() failed
$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
// Keys may appear with a capital first letter. lcfirst them.
- return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
+ return array_map( [ $this->contLang, 'lcfirst' ], array_keys( $cache ) );
}
/**
* @since 1.29
*/
public function updateMessageOverride( Title $title, Content $content = null ) {
- global $wgContLang;
-
$msgText = $this->getMessageTextFromContent( $content );
if ( $msgText === null ) {
$msgText = false; // treat as not existing
$this->replace( $title->getDBkey(), $msgText );
- if ( $wgContLang->hasVariants() ) {
- $wgContLang->updateConversionTable( $title );
+ if ( $this->contLang->hasVariants() ) {
+ $this->contLang->updateConversionTable( $title );
}
}