Merge "Drop wfRunHooks, deprecated since 1.25"
[lhc/web/wiklou.git] / includes / cache / MessageCache.php
index 7a1b988..733b1b7 100644 (file)
@@ -464,13 +464,7 @@ class MessageCache {
 
                $cache = [];
 
-               # Common conditions
-               $conds = [
-                       'page_is_redirect' => 0,
-                       'page_namespace' => NS_MEDIAWIKI,
-               ];
-
-               $mostused = [];
+               $mostused = []; // list of "<cased message key>/<code>"
                if ( $wgAdaptiveMessageCache && $code !== $wgLanguageCode ) {
                        if ( !$this->cache->has( $wgLanguageCode ) ) {
                                $this->load( $wgLanguageCode );
@@ -481,6 +475,14 @@ class MessageCache {
                        }
                }
 
+               // Get the list of software-defined messages in core/extensions
+               $overridable = array_flip( Language::getMessageKeysFor( $wgLanguageCode ) );
+
+               // Common conditions
+               $conds = [
+                       'page_is_redirect' => 0,
+                       'page_namespace' => NS_MEDIAWIKI,
+               ];
                if ( count( $mostused ) ) {
                        $conds['page_title'] = $mostused;
                } elseif ( $code !== $wgLanguageCode ) {
@@ -492,31 +494,28 @@ class MessageCache {
                                $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
                }
 
-               # Conditions to fetch oversized pages to ignore them
-               $bigConds = $conds;
-               $bigConds[] = 'page_len > ' . intval( $wgMaxMsgCacheEntrySize );
-
-               # Load titles for all oversized pages in the MediaWiki namespace
+               // Set the stubs for oversized software-defined messages in the main cache map
                $res = $dbr->select(
                        'page',
                        [ 'page_title', 'page_latest' ],
-                       $bigConds,
+                       array_merge( $conds, [ 'page_len > ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
                        __METHOD__ . "($code)-big"
                );
                foreach ( $res as $row ) {
-                       $cache[$row->page_title] = '!TOO BIG';
+                       $name = $this->contLang->lcfirst( $row->page_title );
+                       // Include entries/stubs for all keys in $mostused in adaptive mode
+                       if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+                               $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
-               $smallConds = $conds;
-               $smallConds[] = 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize );
-
+               // Set the text for small software-defined messages in the main cache map
                $res = $dbr->select(
                        [ 'page', 'revision', 'text' ],
-                       [ 'page_title', 'old_id', 'old_text', 'old_flags' ],
-                       $smallConds,
+                       [ 'page_title', 'page_latest', 'old_id', 'old_text', 'old_flags' ],
+                       array_merge( $conds, [ 'page_len <= ' . intval( $wgMaxMsgCacheEntrySize ) ] ),
                        __METHOD__ . "($code)-small",
                        [],
                        [
@@ -524,23 +523,30 @@ class MessageCache {
                                'text' => [ 'JOIN', 'rev_text_id=old_id' ],
                        ]
                );
-
                foreach ( $res as $row ) {
-                       $text = Revision::getRevisionText( $row );
-                       if ( $text === false ) {
-                               // Failed to fetch data; possible ES errors?
-                               // Store a marker to fetch on-demand as a workaround...
-                               // TODO Use a differnt marker
-                               $entry = '!TOO BIG';
-                               wfDebugLog(
-                                       'MessageCache',
-                                       __METHOD__
-                                       . ": failed to load message page text for {$row->page_title} ($code)"
-                               );
+                       $name = $this->contLang->lcfirst( $row->page_title );
+                       // Include entries/stubs for all keys in $mostused in adaptive mode
+                       if ( $wgAdaptiveMessageCache || isset( $overridable[$name] ) ) {
+                               $text = Revision::getRevisionText( $row );
+                               if ( $text === false ) {
+                                       // Failed to fetch data; possible ES errors?
+                                       // Store a marker to fetch on-demand as a workaround...
+                                       // TODO Use a differnt marker
+                                       $entry = '!TOO BIG';
+                                       wfDebugLog(
+                                               'MessageCache',
+                                               __METHOD__
+                                               . ": failed to load message page text for {$row->page_title} ($code)"
+                                       );
+                               } else {
+                                       $entry = ' ' . $text;
+                               }
+                               $cache[$row->page_title] = $entry;
                        } else {
-                               $entry = ' ' . $text;
+                               // T193271: cache object gets too big and slow to generate.
+                               // At least include revision ID so page changes are reflected in the hash.
+                               $cache['EXCESSIVE'][$row->page_title] = $row->page_latest;
                        }
-                       $cache[$row->page_title] = $entry;
                }
 
                $cache['VERSION'] = MSG_CACHE_VERSION;
@@ -551,6 +557,7 @@ class MessageCache {
                # stored and fetched from memcache.
                $cache['HASH'] = md5( serialize( $cache ) );
                $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry );
+               unset( $cache['EXCESSIVE'] ); // only needed for hash
 
                return $cache;
        }
@@ -584,62 +591,81 @@ class MessageCache {
                }
 
                // (b) Update the shared caches in a deferred update with a fresh DB snapshot
-               DeferredUpdates::addCallableUpdate(
-                       function () use ( $title, $msg, $code ) {
-                               global $wgMaxMsgCacheEntrySize;
-                               // Allow one caller at a time to avoid race conditions
-                               $scopedLock = $this->getReentrantScopedLock(
-                                       $this->clusterCache->makeKey( 'messages', $code )
-                               );
-                               if ( !$scopedLock ) {
-                                       LoggerFactory::getInstance( 'MessageCache' )->error(
-                                               __METHOD__ . ': could not acquire lock to update {title} ({code})',
-                                               [ 'title' => $title, 'code' => $code ] );
-                                       return;
-                               }
-                               // 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,
-                                               $this->mExpiry
-                                       );
-                               }
-                               // Mark this cache as definitely being "latest" (non-volatile) so
-                               // 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( $cache, 'all', $code );
-                               // Release the lock now that the cache is saved
-                               ScopedCallback::consume( $scopedLock );
-
-                               // Relay the purge. Touching this check key expires cache contents
-                               // and local cache (APC) validation hash across all datacenters.
-                               $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
-
-                               // Purge the message in the message blob store
-                               $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
-                               $blobStore = $resourceloader->getMessageBlobStore();
-                               $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
-
-                               Hooks::run( 'MessageCacheReplace', [ $title, $text ] );
-                       },
+               DeferredUpdates::addUpdate(
+                       new MessageCacheUpdate( $code, $title, $msg ),
                        DeferredUpdates::PRESEND
                );
        }
 
+       /**
+        * @param string $code
+        * @param array[] $replacements List of (title, message key) pairs
+        * @throws MWException
+        */
+       public function refreshAndReplaceInternal( $code, array $replacements ) {
+               global $wgMaxMsgCacheEntrySize;
+
+               // Allow one caller at a time to avoid race conditions
+               $scopedLock = $this->getReentrantScopedLock(
+                       $this->clusterCache->makeKey( 'messages', $code )
+               );
+               if ( !$scopedLock ) {
+                       foreach ( $replacements as list( $title ) ) {
+                               LoggerFactory::getInstance( 'MessageCache' )->error(
+                                       __METHOD__ . ': could not acquire lock to update {title} ({code})',
+                                       [ 'title' => $title, 'code' => $code ] );
+                       }
+
+                       return;
+               }
+
+               // 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 individual cache keys should exist and update cache accordingly
+               $newTextByTitle = []; // map of (title => content)
+               foreach ( $replacements as list( $title ) ) {
+                       $page = WikiPage::factory( Title::makeTitle( NS_MEDIAWIKI, $title ) );
+                       $page->loadPageData( $page::READ_LATEST );
+                       $text = $this->getMessageTextFromContent( $page->getContent() );
+                       // Remember the text for the blob store update later on
+                       $newTextByTitle[$title] = $text;
+                       // Note that if $text is false, then $cache should have a !NONEXISTANT entry
+                       if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) {
+                               // 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 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( $cache, 'all', $code );
+               // Release the lock now that the cache is saved
+               ScopedCallback::consume( $scopedLock );
+
+               // Relay the purge. Touching this check key expires cache contents
+               // and local cache (APC) validation hash across all datacenters.
+               $this->wanCache->touchCheckKey( $this->getCheckKey( $code ) );
+
+               // Purge the messages in the message blob store and fire any hook handlers
+               $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader();
+               $blobStore = $resourceloader->getMessageBlobStore();
+               foreach ( $replacements as list( $title, $msg ) ) {
+                       $blobStore->updateMessage( $this->contLang->lcfirst( $msg ) );
+                       Hooks::run( 'MessageCacheReplace', [ $title, $newTextByTitle[$title] ] );
+               }
+       }
+
        /**
         * Is the given cache array expired due to time passing or a version change?
         *
@@ -775,13 +801,12 @@ class MessageCache {
         *   - If boolean and false, create object from the current users language
         *   - If boolean and true, create object from the wikis content language
         *   - If language object, use it as given
-        * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang".
         *
         * @throws MWException When given an invalid key
         * @return string|bool False if the message doesn't exist, otherwise the
         *   message (which can be empty)
         */
-       function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
+       function get( $key, $useDB = true, $langcode = true ) {
                if ( is_int( $key ) ) {
                        // Fix numerical strings that somehow become ints
                        // on their way here
@@ -793,22 +818,14 @@ class MessageCache {
                        return false;
                }
 
-               // For full keys, get the language code from the key
-               $pos = strrpos( $key, '/' );
-               if ( $isFullKey && $pos !== false ) {
-                       $langcode = substr( $key, $pos + 1 );
-                       $key = substr( $key, 0, $pos );
-               }
-
                // Normalise title-case input (with some inlining)
                $lckey = self::normalizeKey( $key );
 
                Hooks::run( 'MessageCache::get', [ &$lckey ] );
 
                // Loop through each language in the fallback list until we find something useful
-               $lang = wfGetLangObj( $langcode );
                $message = $this->getMessageFromFallbackChain(
-                       $lang,
+                       wfGetLangObj( $langcode ),
                        $lckey,
                        !$this->mDisable && $useDB
                );
@@ -900,7 +917,6 @@ class MessageCache {
                                        $this->getMessagePageName( $langcode, $uckey ),
                                        $langcode
                                );
-
                                if ( $message !== false ) {
                                        return $message;
                                }
@@ -975,44 +991,54 @@ class MessageCache {
                $this->load( $code );
 
                $entry = $this->cache->getField( $code, $title );
+
                if ( $entry !== null ) {
+                       // Message page exists as an override of a software messages
                        if ( substr( $entry, 0, 1 ) === ' ' ) {
                                // The message exists and is not '!TOO BIG'
                                return (string)substr( $entry, 1 );
                        } elseif ( $entry === '!NONEXISTENT' ) {
+                               // The text might be '-' or missing due to some data loss
                                return false;
                        }
-                       // 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->cache->setField( $code, $title, ' ' . $message );
-                       } else {
-                               $this->cache->setField( $code, $title, '!NONEXISTENT' );
-                       }
-
-                       return $message;
-               }
-
-               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' => $title, 'code' => $code ] );
+                       // Load the message page, utilizing the individual message cache.
+                       // If the page does not exist, there will be no hook handler fallbacks.
+                       $entry = $this->loadCachedMessagePageEntry(
+                               $title,
+                               $code,
+                               $this->cache->getField( $code, 'HASH' )
+                       );
                } else {
-                       // Try the individual message cache
+                       // Message page does not exist or does not override a software message.
+                       // Load the message page, utilizing the individual message cache.
                        $entry = $this->loadCachedMessagePageEntry(
                                $title,
                                $code,
                                $this->cache->getField( $code, 'HASH' )
                        );
+                       if ( substr( $entry, 0, 1 ) !== ' ' ) {
+                               // Message does not have a MediaWiki page definition; try hook handlers
+                               $message = false;
+                               Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] );
+                               if ( $message !== false ) {
+                                       $this->cache->setField( $code, $title, ' ' . $message );
+                               } else {
+                                       $this->cache->setField( $code, $title, '!NONEXISTENT' );
+                               }
+
+                               return $message;
+                       }
                }
 
                if ( $entry !== false && substr( $entry, 0, 1 ) === ' ' ) {
-                       $this->cache->setField( $code, $title, $entry );
+                       if ( $this->cacheVolatile[$code] ) {
+                               // Make sure that individual keys respect the WAN cache holdoff period too
+                               LoggerFactory::getInstance( 'MessageCache' )->debug(
+                                       __METHOD__ . ': loading volatile key \'{titleKey}\'',
+                                       [ 'titleKey' => $title, 'code' => $code ] );
+                       } else {
+                               $this->cache->setField( $code, $title, $entry );
+                       }
                        // The message exists, so make sure a string is returned
                        return (string)substr( $entry, 1 );
                }
@@ -1029,14 +1055,15 @@ class MessageCache {
         * @return string Either " <MESSAGE>" or "!NONEXISTANT"
         */
        private function loadCachedMessagePageEntry( $dbKey, $code, $hash ) {
+               $fname = __METHOD__;
                return $this->srvCache->getWithSetCallback(
                        $this->srvCache->makeKey( 'messages-big', $hash, $dbKey ),
                        IExpiringStore::TTL_MINUTE,
-                       function () use ( $code, $dbKey, $hash ) {
+                       function () use ( $code, $dbKey, $hash, $fname ) {
                                return $this->wanCache->getWithSetCallback(
                                        $this->bigMessageCacheKey( $hash, $dbKey ),
                                        $this->mExpiry,
-                                       function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code ) {
+                                       function ( $oldValue, &$ttl, &$setOpts ) use ( $dbKey, $code, $fname ) {
                                                // Try loading the message from the database
                                                $dbr = wfGetDB( DB_REPLICA );
                                                $setOpts += Database::getCacheSetOptions( $dbr );
@@ -1053,7 +1080,7 @@ class MessageCache {
                                                        $message = $this->getMessageTextFromContent( $content );
                                                } else {
                                                        LoggerFactory::getInstance( 'MessageCache' )->warning(
-                                                               __METHOD__ . ': failed to load page text for \'{titleKey}\'',
+                                                               $fname . ': failed to load page text for \'{titleKey}\'',
                                                                [ 'titleKey' => $dbKey, 'code' => $code ]
                                                        );
                                                        $message = null;