X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fcache%2FMessageCache.php;h=7a172bead839c923d00184bc393a83aa051cb03a;hb=1facc21e49541fe48945ad9a1e9486bfff32c3de;hp=0854a43e95cc47d0c82ab84dbe4067c8c3796a37;hpb=6638505a857372a29e67c81883ae18e7d0078eb5;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 0854a43e95..7a172bead8 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -46,19 +46,16 @@ class MessageCache { 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 => " " 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 @@ -80,12 +77,6 @@ class MessageCache { /** @var Parser */ protected $mParser; - /** - * Variable for tracking which variables are already loaded - * @var array $mLoadedLanguages - */ - protected $mLoadedLanguages = []; - /** * @var bool $mInParser */ @@ -174,6 +165,8 @@ class MessageCache { $this->clusterCache = $clusterCache; $this->srvCache = $serverCache; + $this->cache = new MapCacheLRU( 5 ); // limit size for sanity + $this->mDisable = !$useDB; $this->mExpiry = $expiry; } @@ -246,8 +239,8 @@ class MessageCache { * 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 ) { @@ -256,7 +249,7 @@ class MessageCache { } # 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; } @@ -279,7 +272,7 @@ class MessageCache { # 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 ); @@ -296,8 +289,8 @@ class MessageCache { $staleCache = $cache; } else { $where[] = 'got from local cache'; + $this->cache->set( $code, $cache ); $success = true; - $this->mCache[$code] = $cache; } if ( !$success ) { @@ -326,7 +319,7 @@ class MessageCache { $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; } @@ -347,7 +340,7 @@ class MessageCache { } 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 ) { @@ -371,13 +364,14 @@ class MessageCache { 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 ); @@ -389,7 +383,7 @@ class MessageCache { /** * @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 ) { @@ -417,7 +411,7 @@ class MessageCache { } $cache = $this->loadFromDB( $code, $mode ); - $this->mCache[$code] = $cache; + $this->cache->set( $code, $cache ); $saveSuccess = $this->saveToCaches( $cache, 'all', $code ); if ( !$saveSuccess ) { @@ -451,7 +445,7 @@ class MessageCache { * 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 ) { @@ -473,10 +467,10 @@ class MessageCache { $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"; } @@ -578,10 +572,10 @@ class MessageCache { // (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 @@ -598,26 +592,31 @@ class MessageCache { [ '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 ); @@ -969,10 +968,13 @@ class MessageCache { * @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 ); @@ -981,93 +983,101 @@ class MessageCache { } // 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 " " 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 ) { @@ -1120,10 +1130,10 @@ class MessageCache { /** * @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, @@ -1192,13 +1202,12 @@ class MessageCache { * * 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(); } /** @@ -1235,12 +1244,12 @@ class MessageCache { 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'] );