From 59242afaa773b0ac62143c2949ad4f8290aa2462 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Thu, 4 Oct 2018 13:14:32 -0700 Subject: [PATCH] messagecache: use MergeableUpdate for the deferred replace() update This combines the load loop for multiple messages for a language code. Bug: T203925 Bug: T193271 Change-Id: Ie5e1e83d6740344b7ca641c99fb3bd4ad5718492 --- autoload.php | 1 + includes/cache/MessageCache.php | 124 +++++++++++++---------- includes/deferred/MessageCacheUpdate.php | 59 +++++++++++ 3 files changed, 131 insertions(+), 53 deletions(-) create mode 100644 includes/deferred/MessageCacheUpdate.php diff --git a/autoload.php b/autoload.php index a0f505623a..ac47093776 100644 --- a/autoload.php +++ b/autoload.php @@ -943,6 +943,7 @@ $wgAutoloadLocalClasses = [ 'Message' => __DIR__ . '/includes/Message.php', 'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php', 'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php', + 'MessageCacheUpdate' => __DIR__ . '/includes/deferred/MessageCacheUpdate.php', 'MessageContent' => __DIR__ . '/includes/content/MessageContent.php', 'MessageLocalizer' => __DIR__ . '/languages/MessageLocalizer.php', 'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php', diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index a08b897514..76d31ff871 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -583,65 +583,83 @@ class MessageCache { // Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date $this->cache->setField( $code, $title, ' ' . $text ); } - $fname = __METHOD__; // (b) Update the shared caches in a deferred update with a fresh DB snapshot - DeferredUpdates::addCallableUpdate( - function () use ( $title, $msg, $code, $fname ) { - 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( - $fname . ': 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? * diff --git a/includes/deferred/MessageCacheUpdate.php b/includes/deferred/MessageCacheUpdate.php new file mode 100644 index 0000000000..c499d082f9 --- /dev/null +++ b/includes/deferred/MessageCacheUpdate.php @@ -0,0 +1,59 @@ + list of (DB key, DB key without code)) */ + private $replacements = []; + + /** + * @param string $code Language code + * @param string $title Message cache key with initial uppercase letter + * @param string $msg Message cache key with initial uppercase letter and without the code + */ + public function __construct( $code, $title, $msg ) { + $this->replacements[$code][] = [ $title, $msg ]; + } + + public function merge( MergeableUpdate $update ) { + /** @var MessageCacheUpdate $update */ + Assert::parameterType( __CLASS__, $update, '$update' ); + + foreach ( $update->replacements as $code => $messages ) { + $this->replacements[$code] = array_merge( $this->replacements[$code] ?? [], $messages ); + } + } + + public function doUpdate() { + $messageCache = MessageCache::singleton(); + foreach ( $this->replacements as $code => $replacements ) { + $messageCache->refreshAndReplaceInternal( $code, $replacements ); + } + } +} -- 2.20.1