X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fcache%2FMessageCache.php;h=f22c860afac35261dc70eac81693c175763daef9;hb=d2ead083a1ea91e2a113da4eeca68a7979e1d839;hp=7945c8be5441106e9107f92892c062c29ceeb591;hpb=8a1152b91365fe51f2505c8dc30dc20319dcafd4;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 7945c8be54..f22c860afa 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -27,23 +27,6 @@ */ define( 'MSG_CACHE_VERSION', 2 ); -/** - * Memcached timeout when loading a key. - * See MessageCache::load() - */ -define( 'MSG_LOAD_TIMEOUT', 60 ); - -/** - * Memcached timeout when locking a key for a writing operation. - * See MessageCache::lock() - */ -define( 'MSG_LOCK_TIMEOUT', 30 ); -/** - * Number of times we will try to acquire a lock from Memcached. - * This comes in addition to MSG_LOCK_TIMEOUT. - */ -define( 'MSG_WAIT_TIMEOUT', 30 ); - /** * Message cache * Performs various MediaWiki namespace-related functions @@ -52,6 +35,11 @@ define( 'MSG_WAIT_TIMEOUT', 30 ); class MessageCache { const FOR_UPDATE = 1; // force message reload + /** How long to wait for memcached locks */ + const WAIT_SEC = 15; + /** How long memcached locks last */ + const LOCK_TTL = 30; + /** * Process local cache of loaded messages that are defined in * MediaWiki namespace. First array level is a language code, @@ -166,7 +154,7 @@ class MessageCache { $this->mExpiry = $expiry; if ( $wgUseLocalMessageCache ) { - $this->localCache = ObjectCache::newAccelerator( array(), CACHE_NONE ); + $this->localCache = ObjectCache::newAccelerator( CACHE_NONE ); } else { $this->localCache = wfGetCache( CACHE_NONE ); } @@ -196,6 +184,7 @@ class MessageCache { */ protected function getLocalCache( $code ) { $cacheKey = wfMemcKey( __CLASS__, $code ); + return $this->localCache->get( $cacheKey ); } @@ -232,8 +221,6 @@ class MessageCache { * @return bool */ function load( $code = false, $mode = null ) { - global $wgUseLocalMessageCache; - if ( !is_string( $code ) ) { # This isn't really nice, so at least make a note about it and try to # fall back @@ -266,25 +253,23 @@ class MessageCache { # or local cache goes out of date (e.g. due to replace() on some other server) list( $hash, $hashVolatile ) = $this->getValidationHash( $code ); - if ( $wgUseLocalMessageCache && $hash ) { - # Try the local cache and check against the cluster hash key... - $cache = $this->getLocalCache( $code ); - if ( !$cache ) { - $where[] = 'local cache is empty'; - } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) { - $where[] = 'local cache has the wrong hash'; - $staleCache = $cache; - } elseif ( $this->isCacheExpired( $cache ) ) { - $where[] = 'local cache is expired'; - $staleCache = $cache; - } elseif ( $hashVolatile ) { - $where[] = 'local cache validation key is expired/volatile'; - $staleCache = $cache; - } else { - $where[] = 'got from local cache'; - $success = true; - $this->mCache[$code] = $cache; - } + # Try the local cache and check against the cluster hash key... + $cache = $this->getLocalCache( $code ); + if ( !$cache ) { + $where[] = 'local cache is empty'; + } elseif ( !isset( $cache['HASH'] ) || $cache['HASH'] !== $hash ) { + $where[] = 'local cache has the wrong hash'; + $staleCache = $cache; + } elseif ( $this->isCacheExpired( $cache ) ) { + $where[] = 'local cache is expired'; + $staleCache = $cache; + } elseif ( $hashVolatile ) { + $where[] = 'local cache validation key is expired/volatile'; + $staleCache = $cache; + } else { + $where[] = 'got from local cache'; + $success = true; + $this->mCache[$code] = $cache; } if ( !$success ) { @@ -292,7 +277,7 @@ class MessageCache { # Try the global cache. If it is empty, try to acquire a lock. If # the lock can't be acquired, wait for the other thread to finish # and then try the global cache a second time. - for ( $failedAttempts = 0; $failedAttempts < 2; $failedAttempts++ ) { + for ( $failedAttempts = 0; $failedAttempts <= 1; $failedAttempts++ ) { if ( $hashVolatile && $staleCache ) { # Do not bother fetching the whole cache blob to avoid I/O. # Instead, just try to get the non-blocking $statusKey lock @@ -326,8 +311,9 @@ class MessageCache { # We need to call loadFromDB. Limit the concurrency to one process. # This prevents the site from going down when the cache expires. - # Note that the slam-protection lock here is non-blocking. - if ( $this->loadFromDBWithLock( $code, $where ) ) { + # Note that the DB slam protection lock here is non-blocking. + $loadStatus = $this->loadFromDBWithLock( $code, $where, $mode ); + if ( $loadStatus === true ) { $success = true; break; } elseif ( $staleCache ) { @@ -337,23 +323,19 @@ class MessageCache { $success = true; break; } elseif ( $failedAttempts > 0 ) { - # Already retried once, still failed, so don't do another lock/unlock cycle + # Already blocked once, so avoid another lock/unlock cycle. # This case will typically be hit if memcached is down, or if - # loadFromDB() takes longer than MSG_WAIT_TIMEOUT + # loadFromDB() takes longer than LOCK_WAIT. $where[] = "could not acquire status key."; break; + } elseif ( $loadStatus === 'cantacquire' ) { + # Wait for the other thread to finish, then retry. Normally, + # the memcached get() will then yeild the other thread's result. + $where[] = 'waited for other thread to complete'; + $this->getReentrantScopedLock( $cacheKey ); } else { - $statusKey = wfMemcKey( 'messages', $code, 'status' ); - $status = $this->mMemc->get( $statusKey ); - if ( $status === 'error' ) { - # Disable cache - break; - } else { - # Wait for the other thread to finish, then retry - $where[] = 'waited for other thread to complete'; - $this->lock( $cacheKey ); - $this->unlock( $cacheKey ); - } + # Disable cache; $loadStatus is 'disabled' + break; } } } @@ -378,49 +360,39 @@ class MessageCache { /** * @param string $code * @param array $where List of wfDebug() comments - * @return bool Lock acquired and loadFromDB() called + * @param integer $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 ) { + protected function loadFromDBWithLock( $code, array &$where, $mode = null ) { global $wgUseLocalMessageCache; - $memCache = $this->mMemc; - + # 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' ); - if ( !$memCache->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ) ) { - return false; // could not acquire lock + $status = $this->mMemc->get( $statusKey ); + if ( $status === 'error' ) { + $where[] = "could not load; method is still globally disabled"; + return 'disabled'; } - # Unlock the status key if there is an exception - $statusUnlocker = new ScopedCallback( function () use ( $memCache, $statusKey ) { - $memCache->delete( $statusKey ); - } ); - # Now let's regenerate $where[] = 'loading from database'; + # Lock the cache to prevent conflicting writes. + # This lock is non-blocking so stale cache can quickly be used. + # Note that load() will call a blocking getReentrantScopedLock() + # after this if it really need to wait for any current thread. $cacheKey = wfMemcKey( 'messages', $code ); - # Lock the cache to prevent conflicting writes - # If this lock fails, it doesn't really matter, it just means the - # write is potentially non-atomic, e.g. the results of a replace() - # may be discarded. - if ( $this->lock( $cacheKey ) ) { - $that = $this; - $mainUnlocker = new ScopedCallback( function () use ( $that, $cacheKey ) { - $that->unlock( $cacheKey ); - } ); - } else { - $mainUnlocker = null; + $scopedLock = $this->getReentrantScopedLock( $cacheKey, 0 ); + if ( !$scopedLock ) { $where[] = 'could not acquire main lock'; + return 'cantacquire'; } - $cache = $this->loadFromDB( $code ); + $cache = $this->loadFromDB( $code, $mode ); $this->mCache[$code] = $cache; $saveSuccess = $this->saveToCaches( $cache, 'all', $code ); - # Unlock - ScopedCallback::consume( $mainUnlocker ); - ScopedCallback::consume( $statusUnlocker ); - if ( !$saveSuccess ) { # Cache save has failed. # There are two main scenarios where this could be a problem: @@ -438,7 +410,7 @@ class MessageCache { # overhead on every request, and thus saves the wiki from # complete downtime under moderate traffic conditions. if ( !$wgUseLocalMessageCache ) { - $memCache->set( $statusKey, 'error', 60 * 5 ); + $this->mMemc->set( $statusKey, 'error', 60 * 5 ); $where[] = 'could not save cache, disabled globally for 5 minutes'; } else { $where[] = "could not save global cache"; @@ -453,13 +425,15 @@ class MessageCache { * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded * on-demand from the database later. * - * @param string $code Language code. - * @return array Loaded messages for storing in caches. + * @param string $code Language code + * @param integer $mode Use MessageCache::FOR_UPDATE to skip process cache + * @return array Loaded messages for storing in caches */ - function loadFromDB( $code ) { + function loadFromDB( $code, $mode = null ) { global $wgMaxMsgCacheEntrySize, $wgLanguageCode, $wgAdaptiveMessageCache; - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( ( $mode == self::FOR_UPDATE ) ? DB_MASTER : DB_SLAVE ); + $cache = array(); # Common conditions @@ -540,7 +514,7 @@ class MessageCache { /** * Updates cache as necessary when message page is changed * - * @param string $title Name of the page changed. + * @param string|bool $title Name of the page changed (false if deleted) * @param mixed $text New contents of the page. */ public function replace( $title, $text ) { @@ -552,32 +526,44 @@ class MessageCache { list( $msg, $code ) = $this->figureMessage( $title ); if ( strpos( $title, '/' ) !== false && $code === $wgLanguageCode ) { - # Content language overrides do not use the / suffix + // Content language overrides do not use the / suffix 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(). $cacheKey = wfMemcKey( 'messages', $code ); - $this->lock( $cacheKey ); + $scopedLock = $this->getReentrantScopedLock( $cacheKey ); $this->load( $code, self::FOR_UPDATE ); $titleKey = wfMemcKey( 'messages', 'individual', $title ); - if ( $text === false ) { - # Article was deleted + // Article was deleted $this->mCache[$code][$title] = '!NONEXISTENT'; - $this->mMemc->delete( $titleKey ); + $this->wanCache->delete( $titleKey ); } elseif ( strlen( $text ) > $wgMaxMsgCacheEntrySize ) { - # Check for size + // Check for size $this->mCache[$code][$title] = '!TOO BIG'; - $this->mMemc->set( $titleKey, ' ' . $text, $this->mExpiry ); + $this->wanCache->set( $titleKey, ' ' . $text, $this->mExpiry ); } else { $this->mCache[$code][$title] = ' ' . $text; - $this->mMemc->delete( $titleKey ); + $this->wanCache->delete( $titleKey ); } - # Update caches - $this->saveToCaches( $this->mCache[$code], 'all', $code ); - $this->unlock( $cacheKey ); + // Mark this cache as definitely "latest" (non-volatile) so + // load() calls do try to refresh the cache with slave data + $this->mCache[$code]['LATEST'] = time(); + + // Update caches if the lock was acquired + if ( $scopedLock ) { + $this->saveToCaches( $this->mCache[$code], 'all', $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 @@ -629,9 +615,7 @@ class MessageCache { * @param string|bool $code Language code (default: false) * @return bool */ - protected function saveToCaches( $cache, $dest, $code = false ) { - global $wgUseLocalMessageCache; - + protected function saveToCaches( array $cache, $dest, $code = false ) { if ( $dest === 'all' ) { $cacheKey = wfMemcKey( 'messages', $code ); $success = $this->mMemc->set( $cacheKey, $cache ); @@ -639,18 +623,14 @@ class MessageCache { $success = true; } - $this->setValidationHash( $code, $cache['HASH'] ); - - # Save to local cache - if ( $wgUseLocalMessageCache ) { - $this->saveToLocalCache( $code, $cache ); - } + $this->setValidationHash( $code, $cache ); + $this->saveToLocalCache( $code, $cache ); return $success; } /** - * Get the md5 used to validate the local disk cache + * Get the md5 used to validate the local APC cache * * @param string $code * @return array (hash or false, bool expiry/volatility status) @@ -658,64 +638,56 @@ class MessageCache { protected function getValidationHash( $code ) { $curTTL = null; $value = $this->wanCache->get( - wfMemcKey( 'messages', $code, 'hash' ), + wfMemcKey( 'messages', $code, 'hash', 'v1' ), $curTTL, array( wfMemcKey( 'messages', $code ) ) ); - $expired = ( $curTTL === null || $curTTL < 0 ); - return array( $value, $expired ); + if ( !$value ) { + // No hash found at all; cache must regenerate to be safe + $hash = false; + $expired = true; + } else { + $hash = $value['hash']; + if ( ( time() - $value['latest'] ) < WANObjectCache::HOLDOFF_TTL ) { + // Cache was recently updated via replace() and should be up-to-date + $expired = false; + } else { + // See if the "check" key was bumped after the hash was generated + $expired = ( $curTTL < 0 ); + } + } + + return array( $hash, $expired ); } /** * Set the md5 used to validate the local disk cache * + * If $cache has a 'LATEST' UNIX timestamp key, then the hash will not + * be treated as "volatile" by getValidationHash() for the next few seconds + * * @param string $code - * @param string $hash + * @param array $cache Cached messages with a version */ - protected function setValidationHash( $code, $hash ) { + protected function setValidationHash( $code, array $cache ) { $this->wanCache->set( - wfMemcKey( 'messages', $code, 'hash' ), - $hash, + wfMemcKey( 'messages', $code, 'hash', 'v1' ), + array( + 'hash' => $cache['HASH'], + 'latest' => isset( $cache['LATEST'] ) ? $cache['LATEST'] : 0 + ), WANObjectCache::TTL_NONE ); } /** - * Represents a write lock on the messages key. - * - * Will retry MessageCache::MSG_WAIT_TIMEOUT times, each operations having - * a timeout of MessageCache::MSG_LOCK_TIMEOUT. - * - * @param string $key - * @return bool Success + * @param string $key A language message cache key that stores blobs + * @param integer $timeout Wait timeout in seconds + * @return null|ScopedCallback */ - function lock( $key ) { - $lockKey = $key . ':lock'; - $acquired = false; - $testDone = false; - for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$acquired; $i++ ) { - $acquired = $this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); - if ( $acquired ) { - break; - } - - # Fail fast if memcached is totally down - if ( !$testDone ) { - $testDone = true; - if ( !$this->mMemc->set( wfMemcKey( 'test' ), 'test', 1 ) ) { - break; - } - } - sleep( 1 ); - } - - return $acquired; - } - - function unlock( $key ) { - $lockKey = $key . ':lock'; - $this->mMemc->delete( $lockKey ); + protected function getReentrantScopedLock( $key, $timeout = self::WAIT_SEC ) { + return $this->mMemc->getScopedLock( $key, $timeout, self::LOCK_TTL, __METHOD__ ); } /** @@ -959,7 +931,7 @@ class MessageCache { # Try the individual message cache $titleKey = wfMemcKey( 'messages', 'individual', $title ); - $entry = $this->mMemc->get( $titleKey ); + $entry = $this->wanCache->get( $titleKey ); if ( $entry ) { if ( substr( $entry, 0, 1 ) === ' ' ) { $this->mCache[$code][$title] = $entry; @@ -973,14 +945,12 @@ class MessageCache { return false; } else { # Corrupt/obsolete entry, delete it - $this->mMemc->delete( $titleKey ); + $this->wanCache->delete( $titleKey ); } } # Try loading it from the database - $revision = Revision::newFromTitle( - Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST - ); + $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) ); if ( $revision ) { $content = $revision->getContent(); if ( !$content ) { @@ -1001,13 +971,13 @@ class MessageCache { wfDebugLog( 'MessageCache', __METHOD__ . ": message content doesn't provide wikitext " - . "(content model: " . $content->getContentHandler() . ")" + . "(content model: " . $content->getModel() . ")" ); $message = false; // negative caching } else { $this->mCache[$code][$title] = ' ' . $message; - $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry ); + $this->wanCache->set( $titleKey, ' ' . $message, $this->mExpiry ); } } } else { @@ -1016,7 +986,7 @@ class MessageCache { if ( $message === false ) { // negative caching $this->mCache[$code][$title] = '!NONEXISTENT'; - $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); + $this->wanCache->set( $titleKey, '!NONEXISTENT', $this->mExpiry ); } return $message;