*/
use MediaWiki\MediaWikiServices;
use Wikimedia\ScopedCallback;
+use MediaWiki\Logger\LoggerFactory;
/**
* MediaWiki message cache structure version.
*/
protected $mCache;
+ /**
+ * @var bool[] Map of (language code => boolean)
+ */
+ protected $mCacheVolatile = [];
+
/**
* Should mean that database cannot be used, but check
* @var bool $mDisable
protected $mExpiry;
/**
- * Message cache has its own parser which it uses to transform
- * messages.
+ * Message cache has its own parser which it uses to transform messages
+ * @var ParserOptions
*/
- protected $mParserOptions, $mParser;
+ protected $mParserOptions;
+ /** @var Parser */
+ protected $mParser;
/**
* Variable for tracking which variables are already loaded
*/
public static function normalizeKey( $key ) {
global $wgContLang;
+
$lckey = strtr( $key, ' ', '_' );
if ( ord( $lckey ) < 128 ) {
$lckey[0] = strtolower( $lckey[0] );
# 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;
# Try the local cache and check against the cluster hash key...
$cache = $this->getLocalCache( $code );
$bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
# Load titles for all oversized pages in the MediaWiki namespace
- $res = $dbr->select( 'page', 'page_title', $bigConds, __METHOD__ . "($code)-big" );
+ $res = $dbr->select(
+ 'page',
+ [ 'page_title', 'page_latest' ],
+ $bigConds,
+ __METHOD__ . "($code)-big"
+ );
foreach ( $res as $row ) {
$cache[$row->page_title] = '!TOO BIG';
+ // At least include revision ID so page changes are reflected in the hash
+ $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
}
# Conditions to load the remaining pages with their contents
* Updates cache as necessary when message page is changed
*
* @param string|bool $title Name of the page changed (false if deleted)
- * @param mixed $text New contents of the page.
+ * @param string|bool $text New contents of the page (false if deleted)
*/
public function replace( $title, $text ) {
global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode;
// 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 );
+ $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) );
+ // Load the messages from the master DB to avoid race conditions
$this->load( $code, self::FOR_UPDATE );
- $titleKey = wfMemcKey( 'messages', 'individual', $title );
+ // Load the new value into the process cache...
if ( $text === false ) {
- // Article was deleted
$this->mCache[$code][$title] = '!NONEXISTENT';
- $this->wanCache->delete( $titleKey );
} elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
- // Check for size
$this->mCache[$code][$title] = '!TOO BIG';
- $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry );
+ // Pre-fill the individual key cache with the known latest message text
+ $key = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title );
+ $this->wanCache->set( $key, " $text", $this->mExpiry );
} else {
$this->mCache[$code][$title] = ' ' . $text;
- $this->wanCache->delete( $titleKey );
}
-
- // Mark this cache as definitely "latest" (non-volatile) so
- // load() calls do try to refresh the cache with replica DB data
+ // Mark this cache as definitely being "latest" (non-volatile) so
+ // load() calls do not try to refresh the cache with replica DB data
$this->mCache[$code]['LATEST'] = time();
// Update caches if the lock was acquired
if ( $scopedLock ) {
$this->saveToCaches( $this->mCache[$code], 'all', $code );
+ } else {
+ LoggerFactory::getInstance( 'MessageCache' )->error(
+ __METHOD__ . ': could not acquire lock to update {title} ({code})',
+ [ 'title' => $title, 'code' => $code ] );
}
ScopedCallback::consume( $scopedLock );
protected function getValidationHash( $code ) {
$curTTL = null;
$value = $this->wanCache->get(
- wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
$curTTL,
[ wfMemcKey( 'messages', $code ) ]
);
- if ( !$value ) {
- // No hash found at all; cache must regenerate to be safe
- $hash = false;
- $expired = true;
- } else {
+ if ( $value ) {
$hash = $value['hash'];
- if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) {
- // Cache was recently updated via replace() and should be up-to-date
+ if ( ( time() - $value['latest'] ) < WANObjectCache::TTL_MINUTE ) {
+ // Cache was recently updated via replace() and should be up-to-date.
+ // That method is only called in the primary datacenter and uses FOR_UPDATE.
+ // Also, it is unlikely that the current datacenter is *now* secondary one.
$expired = false;
} else {
// See if the "check" key was bumped after the hash was generated
$expired = ( $curTTL < 0 );
}
+ } else {
+ // No hash found at all; cache must regenerate to be safe
+ $hash = false;
+ $expired = true;
}
return [ $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
+ * be treated as "volatile" by getValidationHash() for the next few seconds.
+ * This is triggered when $cache is generated using FOR_UPDATE mode.
*
* @param string $code
* @param array $cache Cached messages with a version
*/
protected function setValidationHash( $code, array $cache ) {
$this->wanCache->set(
- wfMemcKey( 'messages', $code, 'hash', 'v1' ),
+ $this->wanCache->makeKey( 'messages', $code, 'hash', 'v1' ),
[
'hash' => $cache['HASH'],
'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0
*/
private function getMessageForLang( $lang, $lckey, $useDB, &$alreadyTried ) {
global $wgContLang;
+
$langcode = $lang->getCode();
// Try checking the database for the requested language
*/
private function getMessagePageName( $langcode, $uckey ) {
global $wgLanguageCode;
+
if ( $langcode === $wgLanguageCode ) {
// Messages created in the content language will not have the /lang extension
return $uckey;
if ( isset( $this->mCache[$code][$title] ) ) {
$entry = $this->mCache[$code][$title];
if ( substr( $entry, 0, 1 ) === ' ' ) {
- // The message exists, so make sure a string
- // is returned.
+ // The message exists, so make sure a string is returned.
return (string)substr( $entry, 1 );
} elseif ( $entry === '!NONEXISTENT' ) {
return false;
}
// Try the individual message cache
- $titleKey = wfMemcKey( 'messages', 'individual', $title );
-
- $curTTL = null;
- $entry = $this->wanCache->get(
- $titleKey,
- $curTTL,
- [ wfMemcKey( 'messages', $code ) ]
- );
- $entry = ( $curTTL >= 0 ) ? $entry : false;
+ $titleKey = $this->wanCache->makeKey( 'messages-big', $this->mCache[$code]['HASH'], $title );
+
+ if ( $this->mCacheVolatile[$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 ] );
+ } else {
+ $entry = $this->wanCache->get( $titleKey );
+ }
- if ( $entry ) {
+ if ( $entry !== false ) {
if ( substr( $entry, 0, 1 ) === ' ' ) {
$this->mCache[$code][$title] = $entry;
// The message exists, so make sure a string is returned
}
}
- // Try loading it from the database
+ // Try loading the message from the database
$dbr = wfGetDB( DB_REPLICA );
$cacheOpts = Database::getCacheSetOptions( $dbr );
// Use newKnownCurrent() to avoid querying revision/user tables
if ( $revision ) {
$content = $revision->getContent();
- if ( !$content ) {
- // A possibly temporary loading failure.
- wfDebugLog(
- 'MessageCache',
- __METHOD__ . ": failed to load message page text for {$title} ($code)"
- );
- $message = null; // no negative caching
- } else {
- // XXX: Is this the right way to turn a Content object into a message?
- // NOTE: $content is typically either WikitextContent, JavaScriptContent or
- // CssContent. MessageContent is *not* used for storing messages, it's
- // only used for wrapping them when needed.
- $message = $content->getWikitextForTransclusion();
-
- if ( $message === false || $message === null ) {
- wfDebugLog(
- 'MessageCache',
- __METHOD__ . ": message content doesn't provide wikitext "
- . "(content model: " . $content->getModel() . ")"
- );
-
- $message = false; // negative caching
- } else {
+ 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
*/
function getParser() {
global $wgParser, $wgParserConf;
+
if ( !$this->mParser && isset( $wgParser ) ) {
# Do some initialisation so that we don't have to do it twice
$wgParser->firstCallInit();
public function parse( $text, $title = null, $linestart = true,
$interface = false, $language = null
) {
+ global $wgTitle;
+
if ( $this->mInParser ) {
return htmlspecialchars( $text );
}
$popts->setTargetLanguage( $language );
if ( !$title || !$title instanceof Title ) {
- global $wgTitle;
wfDebugLog( 'GlobalTitleFail', __METHOD__ . ' called by ' .
wfGetAllCallers( 6 ) . ' with no title set.' );
$title = $wgTitle;
*/
public function getAllMessageKeys( $code ) {
global $wgContLang;
+
$this->load( $code );
if ( !isset( $this->mCache[$code] ) ) {
// Apparently load() failed
$cache = $this->mCache[$code];
unset( $cache['VERSION'] );
unset( $cache['EXPIRY'] );
+ unset( $cache['EXCESSIVE'] );
// Remove any !NONEXISTENT keys
$cache = array_diff( $cache, [ '!NONEXISTENT' ] );
// Keys may appear with a capital first letter. lcfirst them.
return array_map( [ $wgContLang, 'lcfirst' ], array_keys( $cache ) );
}
+
+ /**
+ * Purge message caches when a MediaWiki: page is created, updated, or deleted
+ *
+ * @param Title $title Message page title
+ * @param Content|null $content New content for edit/create, null on deletion
+ * @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 );
+ }
+ }
+
+ /**
+ * @param Content|null $content Content or null if the message page does not exist
+ * @return string|bool|null Returns false if $content is null and null on error
+ */
+ private function getMessageTextFromContent( Content $content = null ) {
+ // @TODO: could skip pseudo-messages like js/css here, based on content model
+ if ( $content ) {
+ // Message page exists...
+ // XXX: Is this the right way to turn a Content object into a message?
+ // NOTE: $content is typically either WikitextContent, JavaScriptContent or
+ // CssContent. MessageContent is *not* used for storing messages, it's
+ // only used for wrapping them when needed.
+ $msgText = $content->getWikitextForTransclusion();
+ if ( $msgText === false || $msgText === null ) {
+ // This might be due to some kind of misconfiguration...
+ $msgText = null;
+ LoggerFactory::getInstance( 'MessageCache' )->warning(
+ __METHOD__ . ": message content doesn't provide wikitext "
+ . "(content model: " . $content->getModel() . ")" );
+ }
+ } else {
+ // Message page does not exist...
+ $msgText = false;
+ }
+
+ return $msgText;
+ }
}