X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fcache%2FMessageCache.php;h=8c1a06813db1f0baad42fb92a604468016a23456;hb=3c3ba5e03e406bbfe0ae7e13991d421512c63dec;hp=f24e8bcde4380b3a462511958d32d1fd05658d65;hpb=c8543fe0bffafec21f2d2774ecc25cdfdcd44c84;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index f24e8bcde4..8c1a06813d 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -25,8 +25,8 @@ * */ define( 'MSG_LOAD_TIMEOUT', 60 ); -define( 'MSG_LOCK_TIMEOUT', 10 ); -define( 'MSG_WAIT_TIMEOUT', 10 ); +define( 'MSG_LOCK_TIMEOUT', 30 ); +define( 'MSG_WAIT_TIMEOUT', 30 ); define( 'MSG_CACHE_VERSION', 1 ); /** @@ -119,15 +119,13 @@ class MessageCache { /** * Try to load the cache from a local file. - * Actual format of the file depends on the $wgLocalMessageCacheSerialized - * setting. * - * @param $hash String: the hash of contents, to check validity. + * @param string $hash the hash of contents, to check validity. * @param $code Mixed: Optional language code, see documenation of load(). - * @return bool on failure. + * @return The cache array */ - function loadFromLocal( $hash, $code ) { - global $wgCacheDirectory, $wgLocalMessageCacheSerialized; + function getLocalCache( $hash, $code ) { + global $wgCacheDirectory; $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; @@ -139,31 +137,19 @@ class MessageCache { return false; // No cache file } - if ( $wgLocalMessageCacheSerialized ) { - // Check to see if the file has the hash specified - $localHash = fread( $file, 32 ); - if ( $hash === $localHash ) { - // All good, get the rest of it - $serialized = ''; - while ( !feof( $file ) ) { - $serialized .= fread( $file, 100000 ); - } - fclose( $file ); - return $this->setCache( unserialize( $serialized ), $code ); - } else { - fclose( $file ); - return false; // Wrong hash + // Check to see if the file has the hash specified + $localHash = fread( $file, 32 ); + if ( $hash === $localHash ) { + // All good, get the rest of it + $serialized = ''; + while ( !feof( $file ) ) { + $serialized .= fread( $file, 100000 ); } + fclose( $file ); + return unserialize( $serialized ); } else { - $localHash = substr( fread( $file, 40 ), 8 ); fclose( $file ); - if ( $hash != $localHash ) { - return false; // Wrong hash - } - - # Require overwrites the member variable or just shadows it? - require( $filename ); - return $this->setCache( $this->mCache, $code ); + return false; // Wrong hash } } @@ -192,55 +178,6 @@ class MessageCache { wfRestoreWarnings(); } - function saveToScript( $array, $hash, $code ) { - global $wgCacheDirectory; - - $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code"; - $tempFilename = $filename . '.tmp'; - wfMkdirParents( $wgCacheDirectory, null, __METHOD__ ); // might fail - - wfSuppressWarnings(); - $file = fopen( $tempFilename, 'w' ); - wfRestoreWarnings(); - - if ( !$file ) { - wfDebug( "Unable to open local cache file for writing\n" ); - return; - } - - fwrite( $file, "mCache = array(" ); - - foreach ( $array as $key => $message ) { - $key = $this->escapeForScript( $key ); - $message = $this->escapeForScript( $message ); - fwrite( $file, "'$key' => '$message',\n" ); - } - - fwrite( $file, ");\n?>" ); - fclose( $file); - rename( $tempFilename, $filename ); - } - - function escapeForScript( $string ) { - $string = str_replace( '\\', '\\\\', $string ); - $string = str_replace( '\'', '\\\'', $string ); - return $string; - } - - /** - * Set the cache to $cache, if it is valid. Otherwise set the cache to false. - * - * @return bool - */ - function setCache( $cache, $code ) { - if ( isset( $cache['VERSION'] ) && $cache['VERSION'] == MSG_CACHE_VERSION ) { - $this->mCache[$code] = $cache; - return true; - } else { - return false; - } - } - /** * Loads messages from caches or from database in this order: * (1) local message cache (if $wgUseLocalMessageCache is enabled) @@ -264,8 +201,6 @@ class MessageCache { function load( $code = false ) { global $wgUseLocalMessageCache; - $exception = null; // deferred error - if( !is_string( $code ) ) { # This isn't really nice, so at least make a note about it and try to # fall back @@ -291,86 +226,161 @@ class MessageCache { # Loading code starts wfProfileIn( __METHOD__ ); $success = false; # Keep track of success + $staleCache = false; # a cache array with expired data, or false if none has been loaded $where = array(); # Debug info, delayed to avoid spamming debug log too much $cacheKey = wfMemcKey( 'messages', $code ); # Key in memc for messages - # (1) local cache + # Local cache # Hash of the contents is stored in memcache, to detect if local cache goes - # out of date (due to update in other thread?) + # out of date (e.g. due to replace() on some other server) if ( $wgUseLocalMessageCache ) { wfProfileIn( __METHOD__ . '-fromlocal' ); $hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) ); if ( $hash ) { - $success = $this->loadFromLocal( $hash, $code ); - if ( $success ) $where[] = 'got from local cache'; + $cache = $this->getLocalCache( $hash, $code ); + if ( !$cache ) { + $where[] = 'local cache is empty or has the wrong hash'; + } elseif ( $this->isCacheExpired( $cache ) ) { + $where[] = 'local cache is expired'; + $staleCache = $cache; + } else { + $where[] = 'got from local cache'; + $success = true; + $this->mCache[$code] = $cache; + } } wfProfileOut( __METHOD__ . '-fromlocal' ); } - # (2) memcache - # Fails if nothing in cache, or in the wrong version. if ( !$success ) { - wfProfileIn( __METHOD__ . '-fromcache' ); - $cache = $this->mMemc->get( $cacheKey ); - $success = $this->setCache( $cache, $code ); - if ( $success ) { - $where[] = 'got from global cache'; - $this->saveToCaches( $cache, false, $code ); - } - wfProfileOut( __METHOD__ . '-fromcache' ); - } + # 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++ ) { + wfProfileIn( __METHOD__ . '-fromcache' ); + $cache = $this->mMemc->get( $cacheKey ); + if ( !$cache ) { + $where[] = 'global cache is empty'; + } elseif ( $this->isCacheExpired( $cache ) ) { + $where[] = 'global cache is expired'; + $staleCache = $cache; + } else { + $where[] = 'got from global cache'; + $this->mCache[$code] = $cache; + $this->saveToCaches( $cache, 'local-only', $code ); + $success = true; + } - # (3) - # Nothing in caches... so we need create one and store it in caches - if ( !$success ) { - $where[] = 'cache is empty'; - $where[] = 'loading from database'; - - $this->lock( $cacheKey ); - # Limit the concurrency of loadFromDB to a single process - # This prevents the site from going down when the cache expires - $statusKey = wfMemcKey( 'messages', $code, 'status' ); - $success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); - if ( $success ) { // acquired lock - $cache = $this->loadFromDB( $code ); - $success = $this->setCache( $cache, $code ); - if ( $success ) { // messages loaded - $success = $this->saveToCaches( $cache, true, $code ); - if ( $success ) { - $this->mMemc->delete( $statusKey ); + wfProfileOut( __METHOD__ . '-fromcache' ); + + if ( $success ) { + # Done, no need to retry + break; + } + + # We need to call loadFromDB. Limit the concurrency to a single + # process. This prevents the site from going down when the cache + # expires. + $statusKey = wfMemcKey( 'messages', $code, 'status' ); + $acquired = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT ); + if ( $acquired ) { + # Unlock the status key if there is an exception + $that = $this; + $statusUnlocker = new ScopedCallback( function() use ( $that, $statusKey ) { + $that->mMemc->delete( $statusKey ); + } ); + + # Now let's regenerate + $where[] = 'loading from database'; + + # 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 ) ) { + $mainUnlocker = new ScopedCallback( function() use ( $that, $cacheKey ) { + $that->unlock( $cacheKey ); + } ); } else { - $this->mMemc->set( $statusKey, 'error', 60 * 5 ); - wfDebug( __METHOD__ . ": set() error: restart memcached server!\n" ); - $exception = new MWException( "Could not save cache for '$code'." ); + $mainUnlocker = null; + $where[] = 'could not acquire main lock'; + } + + $cache = $this->loadFromDB( $code ); + $this->mCache[$code] = $cache; + $success = true; + $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: + # + # - The cache is more than the maximum size (typically + # 1MB compressed). + # + # - Memcached has no space remaining in the relevant slab + # class. This is unlikely with recent versions of + # memcached. + # + # Either way, if there is a local cache, nothing bad will + # happen. If there is no local cache, disabling the message + # cache for all requests avoids 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 ); + $where[] = 'could not save cache, disabled globally for 5 minutes'; + } else { + $where[] = "could not save global cache"; + } } + + # Load from DB complete, no need to retry + break; + } elseif ( $staleCache ) { + # Use the stale cache while some other thread constructs the new one + $where[] = 'using stale cache'; + $this->mCache[$code] = $staleCache; + $success = true; + break; + } elseif ( $failedAttempts > 0 ) { + # Already retried once, still failed, so don't do another lock/unlock cycle + # This case will typically be hit if memcached is down, or if + # loadFromDB() takes longer than MSG_WAIT_TIMEOUT + $where[] = "could not acquire status key."; + break; } else { - $this->mMemc->delete( $statusKey ); - $exception = new MWException( "Could not load cache from DB for '$code'." ); + $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 ); + } } - } else { - $exception = new MWException( "Could not acquire '$statusKey' lock." ); } - $this->unlock( $cacheKey ); } if ( !$success ) { + $where[] = 'loading FAILED - cache is disabled'; $this->mDisable = true; $this->mCache = false; - // This used to go on, but that led to lots of nasty side - // effects like gadgets and sidebar getting cached with their - // default content - if ( $exception instanceof Exception ) { - throw $exception; - } else { - throw new MWException( "MessageCache failed to load messages" ); - } + # 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 } else { # All good, just record the success - $info = implode( ', ', $where ); - wfDebug( __METHOD__ . ": Loading $code... $info\n" ); $this->mLoadedLanguages[$code] = true; } + $info = implode( ', ', $where ); + wfDebug( __METHOD__ . ": Loading $code... $info\n" ); wfProfileOut( __METHOD__ ); return $success; } @@ -380,7 +390,7 @@ class MessageCache { * $wgMaxMsgCacheEntrySize are assigned a special value, and are loaded * on-demand from the database later. * - * @param $code String: language code. + * @param string $code language code. * @return Array: loaded messages for storing in caches. */ function loadFromDB( $code ) { @@ -453,6 +463,7 @@ class MessageCache { } $cache['VERSION'] = MSG_CACHE_VERSION; + $cache['EXPIRY'] = wfTimestamp( TS_MW, time() + $this->mExpiry ); wfProfileOut( __METHOD__ ); return $cache; } @@ -460,7 +471,7 @@ class MessageCache { /** * Updates cache as necessary when message page is changed * - * @param $title String: name of the page changed. + * @param string $title name of the page changed. * @param $text Mixed: new contents of the page. */ public function replace( $title, $text ) { @@ -494,7 +505,7 @@ class MessageCache { } # Update caches - $this->saveToCaches( $this->mCache[$code], true, $code ); + $this->saveToCaches( $this->mCache[$code], 'all', $code ); $this->unlock( $cacheKey ); // Also delete cached sidebar... just in case it is affected @@ -520,22 +531,40 @@ class MessageCache { wfProfileOut( __METHOD__ ); } + /** + * Is the given cache array expired due to time passing or a version change? + */ + protected function isCacheExpired( $cache ) { + if ( !isset( $cache['VERSION'] ) || !isset( $cache['EXPIRY'] ) ) { + return true; + } + if ( $cache['VERSION'] != MSG_CACHE_VERSION ) { + return true; + } + if ( wfTimestampNow() >= $cache['EXPIRY'] ) { + return true; + } + return false; + } + + /** * Shortcut to update caches. * - * @param $cache Array: cached messages with a version. - * @param $memc Bool: Wether to update or not memcache. - * @param $code String: Language code. + * @param $cache array: cached messages with a version. + * @param $dest string: Either "local-only" to save to local caches only + * or "all" to save to all caches. + * @param $code string: Language code. * @return bool on somekind of error. */ - protected function saveToCaches( $cache, $memc = true, $code = false ) { + protected function saveToCaches( $cache, $dest, $code = false ) { wfProfileIn( __METHOD__ ); - global $wgUseLocalMessageCache, $wgLocalMessageCacheSerialized; + global $wgUseLocalMessageCache; $cacheKey = wfMemcKey( 'messages', $code ); - if ( $memc ) { - $success = $this->mMemc->set( $cacheKey, $cache, $this->mExpiry ); + if ( $dest === 'all' ) { + $success = $this->mMemc->set( $cacheKey, $cache ); } else { $success = true; } @@ -544,12 +573,8 @@ class MessageCache { if ( $wgUseLocalMessageCache ) { $serialized = serialize( $cache ); $hash = md5( $serialized ); - $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry ); - if ( $wgLocalMessageCacheSerialized ) { - $this->saveToLocal( $serialized, $hash, $code ); - } else { - $this->saveToScript( $cache, $hash, $code ); - } + $this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash ); + $this->saveToLocal( $serialized, $hash, $code ); } wfProfileOut( __METHOD__ ); @@ -565,11 +590,25 @@ class MessageCache { */ function lock( $key ) { $lockKey = $key . ':lock'; - for ( $i = 0; $i < MSG_WAIT_TIMEOUT && !$this->mMemc->add( $lockKey, 1, MSG_LOCK_TIMEOUT ); $i++ ) { + $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 $i >= MSG_WAIT_TIMEOUT; + return $acquired; } function unlock( $key ) { @@ -578,70 +617,50 @@ class MessageCache { } /** - * Get a message from either the content language or the user language. The fallback - * language order is the users language fallback union the content language fallback. - * This list is then applied to find keys in the following order - * 1) MediaWiki:$key/$langcode (for every language except the content language where - * we look at MediaWiki:$key) - * 2) Built-in messages via the l10n cache which is also in fallback order + * Get a message from either the content language or the user language. * * @param $key String: the message cache key - * @param $useDB Boolean: If true will look for the message in the DB, false only - * get the message from the DB, false to use only the compiled l10n cache. - * @param bool|string|object $langcode Code of the language to get the message for. - * - If string and a valid code, will create a standard language object - * - If string but not a valid code, will create a basic language object - * - 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 $useDB Boolean: get the message from the DB, false to use only + * the localisation + * @param bool|string $langcode Code of the language to get the message for, if + * it is a valid code create a language for that language, + * if it is a string but not a valid code then make a basic + * language object, if it is a false boolean then use the + * current users language (as a fallback for the old + * parameter functionality), or if it is a true boolean + * then use the wikis content language (also as a + * fallback). * @param $isFullKey Boolean: specifies whether $key is a two part key * "msg/lang". * * @throws MWException - * @return string|bool False if the message doesn't exist, otherwise the message + * @return string|bool */ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { global $wgLanguageCode, $wgContLang; - wfProfileIn( __METHOD__ ); - if ( is_int( $key ) ) { // "Non-string key given" exception sometimes happens for numerical strings that become ints somewhere on their way here $key = strval( $key ); } if ( !is_string( $key ) ) { - wfProfileOut( __METHOD__ ); throw new MWException( 'Non-string key given' ); } if ( strval( $key ) === '' ) { # Shortcut: the empty key is always missing - wfProfileOut( __METHOD__ ); return false; } - - # Obtain the initial language object - if ( $isFullKey ) { - $keyParts = explode( '/', $key ); - if ( count( $keyParts ) < 2 ) { - throw new MWException( "Message key '$key' does not appear to be a full key." ); - } - - $langcode = array_pop( $keyParts ); - $key = implode( '/', $keyParts ); - } - - # Obtain a language object for the requested language from the passed language code - # Note that the language code could in fact be a language object already but we assume - # it's a string further below. - $requestedLangObj = wfGetLangObj( $langcode ); - if ( !$requestedLangObj ) { - wfProfileOut( __METHOD__ ); + $lang = wfGetLangObj( $langcode ); + if ( !$lang ) { throw new MWException( "Bad lang code $langcode given" ); } - $langcode = $requestedLangObj->getCode(); + + $langcode = $lang->getCode(); + + $message = false; # Normalise title-case input (with some inlining) $lckey = str_replace( ' ', '_', $key ); @@ -653,37 +672,24 @@ class MessageCache { $uckey = $wgContLang->ucfirst( $lckey ); } - # Loop through each language in the fallback list until we find something useful - $message = false; - # Try the MediaWiki namespace - if ( !$this->mDisable && $useDB ) { - $fallbackChain = Language::getFallbacksIncludingSiteLanguage( $langcode ); - array_unshift( $fallbackChain, $langcode ); - - foreach ( $fallbackChain as $langcode ) { - if ( $langcode === $wgLanguageCode ) { - # Messages created in the content language will not have the /lang extension - $message = $this->getMsgFromNamespace( $uckey, $langcode ); - } else { - $message = $this->getMsgFromNamespace( "$uckey/$langcode", $langcode ); - } - - if ( $message !== false ) { - break; - } + if( !$this->mDisable && $useDB ) { + $title = $uckey; + if( !$isFullKey && ( $langcode != $wgLanguageCode ) ) { + $title .= '/' . $langcode; } + $message = $this->getMsgFromNamespace( $title, $langcode ); } # Try the array in the language object if ( $message === false ) { - $message = $requestedLangObj->getMessage( $lckey ); - if ( is_null ( $message ) ) { + $message = $lang->getMessage( $lckey ); + if ( is_null( $message ) ) { $message = false; } } - # If we still have no message, maybe the key was in fact a full key so try that + # Try the array of another language if( $message === false ) { $parts = explode( '/', $lckey ); # We may get calls for things that are http-urls from sidebar @@ -697,9 +703,15 @@ class MessageCache { } } + # Is this a custom message? Try the default language in the db... + if( ( $message === false || $message === '-' ) && + !$this->mDisable && $useDB && + !$isFullKey && ( $langcode != $wgLanguageCode ) ) { + $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode ); + } + # Final fallback if( $message === false ) { - wfProfileOut( __METHOD__ ); return false; } @@ -713,7 +725,6 @@ class MessageCache { ' ' => "\xc2\xa0", ) ); - wfProfileOut( __METHOD__ ); return $message; } @@ -721,8 +732,12 @@ class MessageCache { * Get a message from the MediaWiki namespace, with caching. The key must * first be converted to two-part lang/msg form if necessary. * - * @param $title String: Message cache key with initial uppercase letter. - * @param $code String: code denoting the language to try. + * Unlike self::get(), this function doesn't resolve fallback chains, and + * some callers require this behavior. LanguageConverter::parseCachedTable() + * and self::get() are some examples in core. + * + * @param string $title Message cache key with initial uppercase letter. + * @param string $code code denoting the language to try. * * @return string|bool False on failure */ @@ -949,9 +964,12 @@ class MessageCache { // Apparently load() failed return null; } - $cache = $this->mCache[$code]; // Copy the cache - unset( $cache['VERSION'] ); // Remove the VERSION key - $cache = array_diff( $cache, array( '!NONEXISTENT' ) ); // Remove any !NONEXISTENT keys + // Remove administrative keys + $cache = $this->mCache[$code]; + unset( $cache['VERSION'] ); + unset( $cache['EXPIRY'] ); + // Remove any !NONEXISTENT keys + $cache = array_diff( $cache, array( '!NONEXISTENT' ) ); // Keys may appear with a capital first letter. lcfirst them. return array_map( array( $wgContLang, 'lcfirst' ), array_keys( $cache ) ); }