Merge "RevisionStoreDbTestBase, remove redundant needsDB override"
[lhc/web/wiklou.git] / includes / cache / MessageCache.php
index 78fb24a..7a1b988 100644 (file)
@@ -55,7 +55,7 @@ class MessageCache {
        /**
         * @var bool[] Map of (language code => boolean)
         */
-       protected $mCacheVolatile = [];
+       protected $cacheVolatile = [];
 
        /**
         * Should mean that database cannot be used, but check
@@ -88,6 +88,8 @@ class MessageCache {
        protected $clusterCache;
        /** @var BagOStuff */
        protected $srvCache;
+       /** @var Language */
+       protected $contLang;
 
        /**
         * Singleton instance
@@ -105,14 +107,16 @@ class MessageCache {
        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()
                        );
                }
 
@@ -135,13 +139,11 @@ class MessageCache {
         * @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;
@@ -153,13 +155,15 @@ class MessageCache {
         * @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;
@@ -169,6 +173,7 @@ class MessageCache {
 
                $this->mDisable = !$useDB;
                $this->mExpiry = $expiry;
+               $this->contLang = $contLang ?? MediaWikiServices::getInstance()->getContentLanguage();
        }
 
        /**
@@ -228,13 +233,13 @@ class MessageCache {
         * (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.
         *
@@ -272,7 +277,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 );
@@ -364,7 +369,7 @@ class MessageCache {
                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
@@ -581,7 +586,7 @@ class MessageCache {
                // (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 )
@@ -592,13 +597,15 @@ 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 );
                                // 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,
@@ -625,7 +632,7 @@ class MessageCache {
                                // 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 ] );
                        },
@@ -858,8 +865,6 @@ class MessageCache {
         * @return string|bool The message, or false if not found
         */
        protected function getMessageFromFallbackChain( $lang, $lckey, $useDB ) {
-               global $wgContLang;
-
                $alreadyTried = [];
 
                // First try the requested language.
@@ -869,7 +874,7 @@ class MessageCache {
                }
 
                // Now try checking the site language.
-               $message = $this->getMessageForLang( $wgContLang, $lckey, $useDB, $alreadyTried );
+               $message = $this->getMessageForLang( $this->contLang, $lckey, $useDB, $alreadyTried );
                return $message;
        }
 
@@ -884,13 +889,11 @@ class MessageCache {
         * @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(
@@ -966,10 +969,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 ( $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 );
@@ -978,6 +984,7 @@ 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 ) {
@@ -989,75 +996,82 @@ class MessageCache {
                        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;
+                                       }
+                               );
+                       }
+               );
        }
 
        /**
@@ -1228,8 +1242,6 @@ class MessageCache {
         * @return array Array of message keys (strings)
         */
        public function getAllMessageKeys( $code ) {
-               global $wgContLang;
-
                $this->load( $code );
                if ( !$this->cache->has( $code ) ) {
                        // Apparently load() failed
@@ -1244,7 +1256,7 @@ class MessageCache {
                $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 ) );
        }
 
        /**
@@ -1255,8 +1267,6 @@ class MessageCache {
         * @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
@@ -1264,8 +1274,8 @@ class MessageCache {
 
                $this->replace( $title->getDBkey(), $msgText );
 
-               if ( $wgContLang->hasVariants() ) {
-                       $wgContLang->updateConversionTable( $title );
+               if ( $this->contLang->hasVariants() ) {
+                       $this->contLang->updateConversionTable( $title );
                }
        }