X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fcache%2FMessageCache.php;h=4e6b2fd39626a0884156eed7e748344f07299574;hb=22806b0a4509e97b56fb52b387e17e3c80fb7eb2;hp=3f78d9a4c8d7c1bbbd8c691d211ffce53717cf91;hpb=dcdb8e463e3b2be121c61c91df13ea36d270a602;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 3f78d9a4c8..4facc20af5 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -89,10 +89,12 @@ class MessageCache { */ protected $mInParser = false; - /** @var BagOStuff */ - protected $mMemc; /** @var WANObjectCache */ protected $wanCache; + /** @var BagOStuff */ + protected $clusterCache; + /** @var BagOStuff */ + protected $srvCache; /** * Singleton instance @@ -109,9 +111,13 @@ class MessageCache { */ public static function singleton() { if ( self::$instance === null ) { - global $wgUseDatabaseMessages, $wgMsgCacheExpiry; + global $wgUseDatabaseMessages, $wgMsgCacheExpiry, $wgUseLocalMessageCache; self::$instance = new self( + MediaWikiServices::getInstance()->getMainWANObjectCache(), wfGetMessageCacheStorage(), + $wgUseLocalMessageCache + ? MediaWikiServices::getInstance()->getLocalServerObjectCache() + : new EmptyBagOStuff(), $wgUseDatabaseMessages, $wgMsgCacheExpiry ); @@ -149,24 +155,25 @@ class MessageCache { } /** - * @param BagOStuff $memCached A cache instance. If none, fall back to CACHE_NONE. - * @param bool $useDB + * @param WANObjectCache $wanCache WAN cache instance + * @param BagOStuff $clusterCache Cluster cache instance + * @param BagOStuff $srvCache Server cache instance + * @param bool $useDB Whether to look for message overrides (e.g. MediaWiki: pages) * @param int $expiry Lifetime for cache. @see $mExpiry. */ - function __construct( BagOStuff $memCached, $useDB, $expiry ) { - global $wgUseLocalMessageCache; + public function __construct( + WANObjectCache $wanCache, + BagOStuff $clusterCache, + BagOStuff $srvCache, + $useDB, + $expiry + ) { + $this->wanCache = $wanCache; + $this->clusterCache = $clusterCache; + $this->srvCache = $srvCache; - $this->mMemc = $memCached; $this->mDisable = !$useDB; $this->mExpiry = $expiry; - - if ( $wgUseLocalMessageCache ) { - $this->localCache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); - } else { - $this->localCache = new EmptyBagOStuff(); - } - - $this->wanCache = ObjectCache::getMainWANInstance(); } /** @@ -203,7 +210,7 @@ class MessageCache { protected function getLocalCache( $code ) { $cacheKey = wfMemcKey( __CLASS__, $code ); - return $this->localCache->get( $cacheKey ); + return $this->srvCache->get( $cacheKey ); } /** @@ -214,7 +221,7 @@ class MessageCache { */ protected function saveToLocalCache( $code, $cache ) { $cacheKey = wfMemcKey( __CLASS__, $code ); - $this->localCache->set( $cacheKey, $cache ); + $this->srvCache->set( $cacheKey, $cache ); } /** @@ -300,7 +307,7 @@ class MessageCache { # below, and use the local stale value if it was not acquired. $where[] = 'global cache is presumed expired'; } else { - $cache = $this->mMemc->get( $cacheKey ); + $cache = $this->clusterCache->get( $cacheKey ); if ( !$cache ) { $where[] = 'global cache is empty'; } elseif ( $this->isCacheExpired( $cache ) ) { @@ -381,12 +388,10 @@ class MessageCache { * @return bool|string True on success or one of ("cantacquire", "disabled") */ protected function loadFromDBWithLock( $code, array &$where, $mode = null ) { - global $wgUseLocalMessageCache; - # If cache updates on all levels fail, give up on message overrides. # This is to avoid easy site outages; see $saveSuccess comments below. $statusKey = wfMemcKey( 'messages', $code, 'status' ); - $status = $this->mMemc->get( $statusKey ); + $status = $this->clusterCache->get( $statusKey ); if ( $status === 'error' ) { $where[] = "could not load; method is still globally disabled"; return 'disabled'; @@ -424,8 +429,8 @@ class MessageCache { * incurring a loadFromDB() overhead on every request, and thus saves the * wiki from complete downtime under moderate traffic conditions. */ - if ( !$wgUseLocalMessageCache ) { - $this->mMemc->set( $statusKey, 'error', 60 * 5 ); + if ( $this->srvCache instanceof EmptyBagOStuff ) { + $this->clusterCache->set( $statusKey, 'error', 60 * 5 ); $where[] = 'could not save cache, disabled globally for 5 minutes'; } else { $where[] = "could not save global cache"; @@ -444,7 +449,7 @@ class MessageCache { * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache * @return array Loaded messages for storing in caches */ - function loadFromDB( $code, $mode = null ) { + protected function loadFromDB( $code, $mode = null ) { global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_REPLICA ); @@ -513,11 +518,12 @@ class MessageCache { 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)" + . ": failed to load message page text for {$row->page_title} ($code)" ); } else { $entry = ' ' . $text; @@ -527,6 +533,10 @@ class MessageCache { $cache['VERSION'] = MSG_CACHE_VERSION; ksort( $cache ); + + # Hash for validating local cache (APC). No need to take into account + # messages larger than $wgMaxMsgCacheEntrySize, since those are only + # stored and fetched from memcache. $cache['HASH'] = md5( serialize( $cache ) ); $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); @@ -536,11 +546,11 @@ class MessageCache { /** * Updates cache as necessary when message page is changed * - * @param string|bool $title Name of the page changed (false if deleted) + * @param string $title Message cache key with initial uppercase letter. * @param string|bool $text New contents of the page (false if deleted) */ public function replace( $title, $text ) { - global $wgMaxMsgCacheEntrySize, $wgContLang, $wgLanguageCode; + global $wgLanguageCode; if ( $this->mDisable ) { return; @@ -552,62 +562,76 @@ class MessageCache { return; } - // Note that if the cache is volatile, load() may trigger a DB fetch. - // In that case we reenter/reuse the existing cache key lock to avoid - // 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(). - $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) ); - // Load the messages from the master DB to avoid race conditions - $this->load( $code, self::FOR_UPDATE ); - - // Load the new value into the process cache... + // (a) Update the process cache with the new message text if ( $text === false ) { + // Page deleted $this->mCache[$code][$title] = '!NONEXISTENT'; - } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { - $this->mCache[$code][$title] = '!TOO BIG'; - // 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 { + // Ignore $wgMaxMsgCacheEntrySize so the process cache is up to date $this->mCache[$code][$title] = ' ' . $text; } - // 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 ); - // Relay the purge to APC and other DCs - $this->wanCache->touchCheckKey( wfMemcKey( 'messages', $code ) ); - - // Also delete cached sidebar... just in case it is affected - $codes = [ $code ]; - if ( $code === 'en' ) { - // Delete all sidebars, like for example on action=purge on the - // sidebar messages - $codes = array_keys( Language::fetchLanguageNames() ); - } - - foreach ( $codes as $code ) { - $sidebarKey = wfMemcKey( 'sidebar', $code ); - $this->wanCache->delete( $sidebarKey ); - } + // (b) Update the shared caches in a deferred update with a fresh DB snapshot + DeferredUpdates::addCallableUpdate( + function () use ( $title, $msg, $code ) { + global $wgContLang, $wgMaxMsgCacheEntrySize; + // Allow one caller at a time to avoid race conditions + $scopedLock = $this->getReentrantScopedLock( wfMemcKey( 'messages', $code ) ); + if ( !$scopedLock ) { + LoggerFactory::getInstance( 'MessageCache' )->error( + __METHOD__ . ': could not acquire lock to update {title} ({code})', + [ 'title' => $title, 'code' => $code ] ); + return; + } + // Load the messages from 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 + $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 + $titleKey = $this->wanCache->makeKey( + 'messages-big', $this->mCache[$code]['HASH'], $title ); + if ( is_string( $text ) && strlen( $text ) > $wgMaxMsgCacheEntrySize ) { + $this->wanCache->set( $titleKey, ' ' . $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(); + // Pre-emptively update the local datacenter cache so things like edit filter and + // blacklist changes are reflect immediately, as 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 ); + // 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( wfMemcKey( 'messages', $code ) ); + // Also delete cached sidebar... just in case it is affected + // @TODO: shouldn't this be $code === $wgLanguageCode? + if ( $code === 'en' ) { + // Purge all language sidebars, e.g. on ?action=purge to the sidebar messages + $codes = array_keys( Language::fetchLanguageNames() ); + } else { + // Purge only the sidebar for this language + $codes = [ $code ]; + } + foreach ( $codes as $code ) { + $this->wanCache->delete( wfMemcKey( 'sidebar', $code ) ); + } - // Update the message in the message blob store - $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader(); - $blobStore = $resourceloader->getMessageBlobStore(); - $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) ); + // Purge the message in the message blob store + $resourceloader = RequestContext::getMain()->getOutput()->getResourceLoader(); + $blobStore = $resourceloader->getMessageBlobStore(); + $blobStore->updateMessage( $wgContLang->lcfirst( $msg ) ); - Hooks::run( 'MessageCacheReplace', [ $title, $text ] ); + Hooks::run( 'MessageCacheReplace', [ $title, $text ] ); + }, + DeferredUpdates::PRESEND + ); } /** @@ -642,7 +666,7 @@ class MessageCache { protected function saveToCaches( array $cache, $dest, $code = false ) { if ( $dest === 'all' ) { $cacheKey = wfMemcKey( 'messages', $code ); - $success = $this->mMemc->set( $cacheKey, $cache ); + $success = $this->clusterCache->set( $cacheKey, $cache ); $this->setValidationHash( $code, $cache ); } else { $success = true; @@ -714,7 +738,7 @@ class MessageCache { * @return null|ScopedCallback */ protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) { - return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ ); + return $this->clusterCache->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ ); } /** @@ -839,7 +863,7 @@ class MessageCache { $alreadyTried = []; - // First try the requested language. + // First try the requested language. $message = $this->getMessageForLang( $lang, $lckey, $useDB, $alreadyTried ); if ( $message !== false ) { return $message; @@ -944,6 +968,7 @@ class MessageCache { */ public function getMsgFromNamespace( $title, $code ) { $this->load( $code ); + if ( isset( $this->mCache[$code][$title] ) ) { $entry = $this->mCache[$code][$title]; if ( substr( $entry, 0, 1 ) === ' ' ) { @@ -957,7 +982,7 @@ class MessageCache { } else { // XXX: This is not cached in process cache, should it? $message = false; - Hooks::run( 'MessagesPreLoad', [ $title, &$message ] ); + Hooks::run( 'MessagesPreLoad', [ $title, &$message, $code ] ); if ( $message !== false ) { return $message; }