diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 731ba0b59eb..c7033d13dab 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -110,6 +110,9 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN set $wgCacheDirectory to get a faster CDB-based implementation. * Expanded the number of variables which can be set in the extension messages files. +* Added a feature to allow per-article process pool size control for the parsing + task, to limit resource usage when the cache for a heavily-viewed article is + invalidated. Requires an external daemon. === Bug fixes in 1.16 === diff --git a/includes/Article.php b/includes/Article.php index a7b75c5f734..4b231598a4d 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -40,6 +40,7 @@ class Article { var $mTouched = '19700101000000'; //!< var $mUser = -1; //!< Not loaded var $mUserText = ''; //!< + var $mParserOptions; //!< /**@}}*/ /** @@ -718,38 +719,40 @@ class Article { } /** - * This is the default action of the script: just view the page of - * the given title. + * This is the default action of the index.php entry point: just view the + * page of the given title. */ public function view() { global $wgUser, $wgOut, $wgRequest, $wgContLang; global $wgEnableParserCache, $wgStylePath, $wgParser; - global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; - global $wgDefaultRobotPolicy; - - # Let the parser know if this is the printable version - if( $wgOut->isPrintable() ) { - $wgOut->parserOptions()->setIsPrintable( true ); - } + global $wgUseTrackbacks; wfProfileIn( __METHOD__ ); # Get variables from query string $oldid = $this->getOldID(); + $parserCache = ParserCache::singleton(); + + $parserOptions = clone $this->getParserOptions(); + # Render printable version, use printable version cache + if ( $wgOut->isPrintable() ) { + $parserOptions->setIsPrintable( true ); + } # Try client and file cache if( $oldid === 0 && $this->checkTouched() ) { global $wgUseETag; if( $wgUseETag ) { - $parserCache = ParserCache::singleton(); - $wgOut->setETag( $parserCache->getETag($this, $wgOut->parserOptions()) ); + $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); } # Is is client cached? if( $wgOut->checkLastModified( $this->getTouched() ) ) { + wfDebug( __METHOD__.": done 304\n" ); wfProfileOut( __METHOD__ ); return; # Try file cache } else if( $this->tryFileCache() ) { + wfDebug( __METHOD__.": done file cache\n" ); # tell wgOut that output is taken care of $wgOut->disable(); $this->viewUpdates(); @@ -758,80 +761,251 @@ class Article { } } - $ns = $this->mTitle->getNamespace(); # shortcut $sk = $wgUser->getSkin(); # getOldID may want us to redirect somewhere else if( $this->mRedirectUrl ) { $wgOut->redirect( $this->mRedirectUrl ); + wfDebug( __METHOD__.": redirecting due to oldid\n" ); wfProfileOut( __METHOD__ ); return; } - $diff = $wgRequest->getVal( 'diff' ); - $rcid = $wgRequest->getVal( 'rcid' ); - $rdfrom = $wgRequest->getVal( 'rdfrom' ); - $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); - $purge = $wgRequest->getVal( 'action' ) == 'purge'; - $return404 = false; - $wgOut->setArticleFlag( true ); + $wgOut->setRobotPolicy( $this->getRobotPolicyForView() ); + # Set page title (may be overridden by DISPLAYTITLE) + $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - # Discourage indexing of printable versions, but encourage following - if( $wgOut->isPrintable() ) { - $policy = 'noindex,follow'; - } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { - $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()]; - } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { - # Honour customised robot policies for this namespace - $policy = $wgNamespaceRobotPolicies[$ns]; - } else { - $policy = $wgDefaultRobotPolicy; - } - $wgOut->setRobotPolicy( $policy ); - - # Allow admins to see deleted content if explicitly requested - $delId = $diff ? $diff : $oldid; - $unhide = $wgRequest->getInt('unhide') == 1; - # If we got diff and oldid in the query, we want to see a - # diff page instead of the article. - if( !is_null( $diff ) ) { - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - - $htmldiff = $wgRequest->getVal( 'htmldiff' , false); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff, $unhide ); - // DifferenceEngine directly fetched the revision: - $this->mRevIdFetched = $de->mNewid; - $de->showDiffPage( $diffOnly ); - - // Needed to get the page's current revision - $this->loadPageData(); - if( $diff == 0 || $diff == $this->mLatest ) { - # Run view updates for current revision only - $this->viewUpdates(); - } + # If we got diff in the query, we want to see a diff page instead of the article. + if( !is_null( $wgRequest->getVal( 'diff' ) ) ) { + wfDebug( __METHOD__.": showing diff page\n" ); + $this->showDiffPage(); wfProfileOut( __METHOD__ ); return; } - if( $ns == NS_USER || $ns == NS_USER_TALK ) { - # User/User_talk subpages are not modified. (bug 11443) - if( !$this->mTitle->isSubpage() ) { - $block = new Block(); - if( $block->load( $this->mTitle->getBaseText() ) ) { - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - } - } - } - # Should the parser cache be used? - $pcache = $this->useParserCache( $oldid ); - wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); + $useParserCache = $this->useParserCache( $oldid ); + wfDebug( 'Article::view using parser cache: ' . ($useParserCache ? 'yes' : 'no' ) . "\n" ); if( $wgUser->getOption( 'stubthreshold' ) ) { wfIncrStats( 'pcache_miss_stub' ); } - $wasRedirected = false; + # For the main page, overwrite the
and don't parse + $m = array(); + preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); + $wgOut->addHTML( "\n" ); + $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); + $wgOut->addHTML( "\n\n" ); + } + } + + /** + * Get the robot policy to be used for the current action=view request. + */ + public function getRobotPolicyForView() { + global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; + global $wgDefaultRobotPolicy, $wgRequest; + + $ns = $this->mTitle->getNamespace(); + if( $ns == NS_USER || $ns == NS_USER_TALK ) { + # Don't index user and user talk pages for blocked users (bug 11443) + if( !$this->mTitle->isSubpage() ) { + $block = new Block(); + if( $block->load( $this->mTitle->getText() ) ) { + return 'noindex,nofollow'; + } + } + } + + if( $this->getID() === 0 || $this->getOldID() ) { + return 'noindex,nofollow'; + } elseif( $wgOut->isPrintable() ) { + # Discourage indexing of printable versions, but encourage following + return 'noindex,follow'; + } elseif( $wgRequest->getInt('curid') ) { + # For ?curid=x urls, disallow indexing + return 'noindex,follow'; + } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + return $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()]; + } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) { + # Honour customised robot policies for this namespace + return $wgNamespaceRobotPolicies[$ns]; + } else { + return $wgDefaultRobotPolicy; + } + } + + /** + * If this request is a redirect view, send "redirected from" subtitle to + * $wgOut. Returns true if the header was needed, false if this is not a + * redirect view. Handles both local and remote redirects. + */ + public function showRedirectedFromHeader() { + global $wgOut, $wgUser, $wgRequest, $wgRedirectSources; + + $rdfrom = $wgRequest->getVal( 'rdfrom' ); + $sk = $wgUser->getSkin(); if( isset( $this->mRedirectedFrom ) ) { // This is an internally redirected page view. // We'll need a backlink to the source page for navigation. @@ -856,226 +1030,156 @@ class Article { $wgOut->addLink( array( 'rel' => 'canonical', 'href' => $this->mTitle->getLocalURL() ) ); - $wasRedirected = true; + return true; } - } elseif( !empty( $rdfrom ) ) { + } elseif( $rdfrom ) { // This is an externally redirected view, from some other wiki. // If it was reported from a trusted site, supply a backlink. - global $wgRedirectSources; if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); - $wasRedirected = true; + return true; } } + return false; + } - # Allow a specific header on talk pages, like [[MediaWiki:Talkpagetext]] + /** + * Show a header specific to the namespace currently being viewed, like + * [[MediaWiki:Talkpagetext]]. For Article::view(). + */ + public function showNamespaceHeader() { + global $wgOut; if( $this->mTitle->isTalkPage() ) { $msg = wfMsgNoTrans( 'talkpageheader' ); if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) { $wgOut->wrapWikiMsg( "\n$1", array( 'talkpageheader' ) ); } } + } - $outputDone = false; - wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); - if( $pcache && $wgOut->tryParserCache( $this ) ) { - // Ensure that UI elements requiring revision ID have - // the correct version information. - $wgOut->setRevisionId( $this->mLatest ); - $outputDone = true; - } - # Fetch content and check for errors - if( !$outputDone ) { - # If the article does not exist and was deleted/moved, show the log - if( $this->getID() == 0 ) { - $this->showLogs(); - } - $text = $this->getContent(); - // For now, check also for ID until getContent actually returns - // false for pages that do not exists - if( $text === false || $this->getID() === 0 ) { - # Failed to load, replace text with error message - $t = $this->mTitle->getPrefixedText(); - if( $oldid ) { - $d = wfMsgExt( 'missingarticle-rev', 'escape', $oldid ); - $text = wfMsgExt( 'missing-article', 'parsemag', $t, $d ); - // Always use page content for pages in the MediaWiki namespace - // since it contains the default message - } elseif ( $this->mTitle->getNamespace() != NS_MEDIAWIKI ) { - $text = wfMsgExt( 'noarticletext', 'parsemag' ); - } - } - - # Non-existent pages - if( $this->getID() === 0 ) { - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $text = "\n$text\n"; - if( !$this->hasViewableContent() ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $return404 = true; - } - } - - if( $return404 ) { - $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); - } - - # Another whitelist check in case oldid is altering the title - if( !$this->mTitle->userCanRead() ) { - $wgOut->loginToUse(); - $wgOut->output(); - $wgOut->disable(); - wfProfileOut( __METHOD__ ); - return; - } - - # For ?curid=x urls, disallow indexing - if( $wgRequest->getInt('curid') ) - $wgOut->setRobotPolicy( 'noindex,follow' ); - - # We're looking at an old revision - if( !empty( $oldid ) ) { - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - if( is_null( $this->mRevision ) ) { - // FIXME: This would be a nice place to load the 'no such page' text. - } else { - $this->setOldSubtitle( $oldid ); - # Allow admins to see deleted content if explicitly requested - if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - // If the user is not allowed to see it... - if( !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n$1\n", - 'rev-deleted-text-permission' ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - wfProfileOut( __METHOD__ ); - return; - // If the user needs to confirm that they want to see it... - } else if( !$unhide ) { - # Give explanation and add a link to view the revision... - $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" ); - $wgOut->wrapWikiMsg( "\n$1\n", - array('rev-deleted-text-unhide',$link) ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - wfProfileOut( __METHOD__ ); - return; - // We are allowed to see... - } else { - $wgOut->wrapWikiMsg( "\n$1\n", - 'rev-deleted-text-view' ); - } - } - // Is this the current revision and otherwise cacheable? Try the parser cache... - if( $oldid === $this->getLatest() && $this->useParserCache( false ) - && $wgOut->tryParserCache( $this ) ) - { - $outputDone = true; - } - } - } - - // Ensure that UI elements requiring revision ID have - // the correct version information. - $wgOut->setRevisionId( $this->getRevIdFetched() ); - - if( $outputDone ) { - // do nothing... - // Pages containing custom CSS or JavaScript get special treatment - } else if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { - $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) ); - // Give hooks a chance to customise the output - if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { - // Wrap the whole lot in aand don't parse - $m = array(); - preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); - $wgOut->addHTML( "\n" ); - $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); - $wgOut->addHTML( "\n\n" ); - } - } else if( $rt = Title::newFromRedirectArray( $text ) ) { # get an array of redirect targets - # Don't append the subtitle if this was an old revision - $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); - $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); - $wgOut->addParserOutputNoText( $parseout ); - } else if( $pcache ) { - # Display content and save to parser cache - $this->outputWikiText( $text ); - } else { - # Display content, don't attempt to save to parser cache - # Don't show section-edit links on old revisions... this way lies madness. - if( !$this->isCurrent() ) { - $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false ); - } - # Display content and don't save to parser cache - # With timing hack -- TS 2006-07-26 - $time = -wfTime(); - $this->outputWikiText( $text, false ); - $time += wfTime(); - - # Timing hack - if( $time > 3 ) { - wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, - $this->mTitle->getPrefixedDBkey())); - } - - if( !$this->isCurrent() ) { - $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); - } - } - } - /* title may have been set from the cache */ - $t = $wgOut->getPageTitle(); - if( empty( $t ) ) { - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - - # For the main page, overwrite theelement with the con- - # tents of 'pagetitle-view-mainpage' instead of the default (if - # that's not empty). - if( $this->mTitle->equals( Title::newMainPage() ) && - wfMsgForContent( 'pagetitle-view-mainpage' ) !== '' ) { - $wgOut->setHTMLTitle( wfMsgForContent( 'pagetitle-view-mainpage' ) ); - } - } - + /** + * Show the footer section of an ordinary page view + */ + public function showViewFooter() { + global $wgOut, $wgUseTrackbacks, $wgRequest; # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if( $ns == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { + if( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { $wgOut->addWikiMsg('anontalkpagetext'); } # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if( !empty( $rcid ) && $this->mTitle->exists() && $this->mTitle->quickUserCan( 'patrol' ) ) { - $wgOut->addHTML( - " " . - wfMsgHtml( - 'markaspatrolledlink', - $sk->link( - $this->mTitle, - wfMsgHtml( 'markaspatrolledtext' ), - array(), - array( - 'action' => 'markpatrolled', - 'rcid' => $rcid - ), - array( 'known', 'noclasses' ) - ) - ) . - '' - ); - } + $this->showPatrolFooter(); # Trackbacks if( $wgUseTrackbacks ) { $this->addTrackbacks(); } - - $this->viewUpdates(); - wfProfileOut( __METHOD__ ); } - protected function showLogs() { + /** + * If patrol is possible, output a patrol UI box. This is called from the + * footer section of ordinary page views. If patrol is not possible or not + * desired, does nothing. + */ + public function showPatrolFooter() { + global $wgOut, $wgRequest; + $rcid = $wgRequest->getVal( 'rcid' ); + + if( !$rcid || !$this->mTitle->exists() || !$this->mTitle->quickUserCan( 'patrol' ) ) { + return; + } + + $wgOut->addHTML( + "" . + wfMsgHtml( + 'markaspatrolledlink', + $sk->link( + $this->mTitle, + wfMsgHtml( 'markaspatrolledtext' ), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid + ), + array( 'known', 'noclasses' ) + ) + ) . + '' + ); + } + + /** + * Show the error text for a missing article. For articles in the MediaWiki + * namespace, show the default message text. To be called from Article::view(). + */ + public function showMissingArticle() { + global $wgOut, $wgRequest; + # Show delete and move logs + $this->showLogs(); + + # Show error message + $oldid = $this->getOldID(); + if( $oldid ) { + $text = wfMsgNoTrans( 'missing-article', + $this->mTitle->getPrefixedText(), + wfMsgNoTrans( 'missingarticle-rev', $oldid ) ); + } elseif ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) { + // Use the default message text + $text = $this->getContent(); + } else { + $text = wfMsgNoTrans( 'noarticletext' ); + } + $text = "\n$text\n"; + if( !$this->hasViewableContent() ) { + // If there's no backing content, send a 404 Not Found + // for better machine handling of broken links. + $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); + } + $wgOut->addWikiText( $text ); + } + + /** + * If the revision requested for view is deleted, check permissions. + * Send either an error message or a warning header to $wgOut. + * Returns true if the view is allowed, false if not. + */ + public function showDeletedRevisionHeader() { + global $wgOut, $wgRequest; + + if( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + // Not deleted + return true; + } + + // If the user is not allowed to see it... + if( !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\n$1\n", + 'rev-deleted-text-permission' ); + return false; + // If the user needs to confirm that they want to see it... + } else if( $wgRequest->getInt('unhide') != 1 ) { + # Give explanation and add a link to view the revision... + $oldid = intval( $this->getOldID() ); + $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" ); + $wgOut->wrapWikiMsg( "\n$1\n", + array('rev-deleted-text-unhide',$link) ); + return false; + // We are allowed to see... + } else { + $wgOut->wrapWikiMsg( "\n$1\n", + 'rev-deleted-text-view' ); + return true; + } + } + + /** + * Show an excerpt from the deletion and move logs. To be called from the + * header section on page views of missing pages. + */ + public function showLogs() { global $wgUser, $wgOut; $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut ); $pager = new LogPager( $loglist, array('move', 'delete'), false, @@ -1104,7 +1208,7 @@ class Article { /* * Should the parser cache be used? */ - protected function useParserCache( $oldid ) { + public function useParserCache( $oldid ) { global $wgUser, $wgEnableParserCache; return $wgEnableParserCache @@ -1115,6 +1219,64 @@ class Article { && !$this->mTitle->isCssJsSubpage(); } + /** + * Execute the uncached parse for action=view + */ + public function doViewParse() { + global $wgOut; + $oldid = $this->getOldID(); + $useParserCache = $this->useParserCache( $oldid ); + $parserOptions = clone $this->getParserOptions(); + # Render printable version, use printable version cache + $parserOptions->setIsPrintable( $wgOut->isPrintable() ); + # Don't show section-edit links on old revisions... this way lies madness. + $parserOptions->setEditSection( $this->isCurrent() ); + $useParserCache = $this->useParserCache( $oldid ); + $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions ); + } + + /** + * Try to fetch an expired entry from the parser cache. If it is present, + * output it and return true. If it is not present, output nothing and + * return false. This is used as a callback function for + * PoolCounter::executeProtected(). + */ + public function tryDirtyCache() { + global $wgOut; + $parserCache = ParserCache::singleton(); + $options = $this->getParserOptions(); + $options->setIsPrintable( $wgOut->isPrintable() ); + $output = $parserCache->getDirty( $this, $options ); + if ( $output ) { + wfDebug( __METHOD__.": sending dirty output\n" ); + wfDebugLog( 'dirty', "dirty output " . $parserCache->getKey( $this, $options ) . "\n" ); + $wgOut->setSquidMaxage( 0 ); + $wgOut->addParserOutput( $output ); + $wgOut->addHTML( "\n" ); + return true; + } else { + wfDebugLog( 'dirty', "dirty missing\n" ); + wfDebug( __METHOD__.": no dirty cache\n" ); + return false; + } + } + + /** + * Show an error page for an error from the pool counter. + * @param $status Status + */ + public function showPoolError( $status ) { + global $wgOut; + $wgOut->clearHTML(); // for release() errors + $wgOut->enableClientCache( false ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->addWikiText( + '' . + $status->getWikiText( false, 'view-pool-error' ) . + '' + ); + } + /** * View redirect * @param $target Title object or Array of destination(s) to redirect @@ -1511,7 +1673,6 @@ class Article { * @deprecated use Article::doEdit() */ function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) { - wfDeprecated( __METHOD__ ); $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | ( $minor ? EDIT_MINOR : 0 ) | ( $forceBot ? EDIT_FORCE_BOT : 0 ); @@ -2940,9 +3101,7 @@ class Article { $edit->revid = $revid; $edit->newText = $text; $edit->pst = $this->preSaveTransform( $text ); - $options = new ParserOptions; - $options->setTidy( true ); - $options->enableLimitReport(); + $options = $this->getParserOptions(); $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid ); $edit->oldText = $this->getContent(); $this->mPreparedEdit = $edit; @@ -2980,9 +3139,7 @@ class Article { # Save it to the parser cache if( $wgEnableParserCache ) { - $popts = new ParserOptions; - $popts->setTidy( true ); - $popts->enableLimitReport(); + $popts = $this->getParserOptions(); $parserCache = ParserCache::singleton(); $parserCache->save( $editInfo->output, $this, $popts ); } @@ -3674,11 +3831,10 @@ class Article { * @param $text String * @param $cache Boolean */ - public function outputWikiText( $text, $cache = true ) { + public function outputWikiText( $text, $cache = true, $parserOptions = false ) { global $wgOut; - $parserOutput = $this->outputFromWikitext( $text, $cache ); - + $parserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions ); $wgOut->addParserOutput( $parserOutput ); } @@ -3687,19 +3843,27 @@ class Article { * output instead of sending it straight to $wgOut. Makes things nice and simple for, * say, embedding thread pages within a discussion system (LiquidThreads) */ - public function outputFromWikitext( $text, $cache = true ) { + public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) { global $wgParser, $wgOut, $wgEnableParserCache, $wgUseFileCache; - $popts = $wgOut->parserOptions(); - $popts->setTidy(true); - $popts->enableLimitReport(); + if ( !$parserOptions ) { + $parserOptions = $this->getParserOptions(); + } + + $time = -wfTime(); $parserOutput = $wgParser->parse( $text, $this->mTitle, - $popts, true, true, $this->getRevIdFetched() ); - $popts->setTidy(false); - $popts->enableLimitReport( false ); + $parserOptions, true, true, $this->getRevIdFetched() ); + $time += wfTime(); + + # Timing hack + if( $time > 3 ) { + wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time, + $this->mTitle->getPrefixedDBkey())); + } + if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache = ParserCache::singleton(); - $parserCache->save( $parserOutput, $this, $popts ); + $parserCache->save( $parserOutput, $this, $parserOptions ); } // Make sure file cache is not used on uncacheable content. // Output that has magic words in it can still use the parser cache @@ -3707,51 +3871,68 @@ class Article { if( $parserOutput->getCacheTime() == -1 || $parserOutput->containsOldMagic() ) { $wgUseFileCache = false; } + $this->doCascadeProtectionUpdates( $parserOutput ); + return $parserOutput; + } - if( $this->isCurrent() && !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) { - // templatelinks table may have become out of sync, - // especially if using variable-based transclusions. - // For paranoia, check if things have changed and if - // so apply updates to the database. This will ensure - // that cascaded protections apply as soon as the changes - // are visible. + /** + * Get parser options suitable for rendering the primary article wikitext + */ + public function getParserOptions() { + global $wgUser; + if ( !$this->mParserOptions ) { + $this->mParserOptions = new ParserOptions( $wgUser ); + $this->mParserOptions->setTidy( true ); + $this->mParserOptions->enableLimitReport(); + } + return $this->mParserOptions; + } - # Get templates from templatelinks - $id = $this->mTitle->getArticleID(); + protected function doCascadeProtectionUpdates( $parserOutput ) { + if( !$this->isCurrent() || wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) { + return; + } - $tlTemplates = array(); + // templatelinks table may have become out of sync, + // especially if using variable-based transclusions. + // For paranoia, check if things have changed and if + // so apply updates to the database. This will ensure + // that cascaded protections apply as soon as the changes + // are visible. - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks' ), - array( 'tl_namespace', 'tl_title' ), - array( 'tl_from' => $id ), - __METHOD__ ); + # Get templates from templatelinks + $id = $this->mTitle->getArticleID(); - global $wgContLang; - foreach( $res as $row ) { - $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; - } + $tlTemplates = array(); - # Get templates from parser output. - $poTemplates = array(); - foreach ( $parserOutput->getTemplates() as $ns => $templates ) { - foreach ( $templates as $dbk => $id ) { - $poTemplates["$ns:$dbk"] = true; - } - } + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array( 'templatelinks' ), + array( 'tl_namespace', 'tl_title' ), + array( 'tl_from' => $id ), + __METHOD__ ); - # Get the diff - # Note that we simulate array_diff_key in PHP <5.0.x - $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); + global $wgContLang; + foreach( $res as $row ) { + $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; + } - if( count( $templates_diff ) > 0 ) { - # Whee, link updates time. - $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); - $u->doUpdate(); + # Get templates from parser output. + $poTemplates = array(); + foreach ( $parserOutput->getTemplates() as $ns => $templates ) { + foreach ( $templates as $dbk => $id ) { + $poTemplates["$ns:$dbk"] = true; } } - - return $parserOutput; + + # Get the diff + # Note that we simulate array_diff_key in PHP <5.0.x + $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); + + if( count( $templates_diff ) > 0 ) { + # Whee, link updates time. + $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); + $u->doUpdate(); + } } /** @@ -3811,16 +3992,6 @@ class Article { } } - function tryParserCache( $parserOptions ) { - $parserCache = ParserCache::singleton(); - $parserOutput = $parserCache->get( $this, $parserOptions ); - if ( $parserOutput !== false ) { - return $parserOutput; - } else { - return false; - } - } - /** Lightweight method to get the parser output for a page, checking the parser cache * and so on. Doesn't consider most of the stuff that Article::view is forced to * consider, so it's not appropriate to use there. */ @@ -3828,26 +3999,26 @@ class Article { global $wgEnableParserCache, $wgUser, $wgOut; // Should the parser cache be used? - $pcache = $wgEnableParserCache && + $useParserCache = $wgEnableParserCache && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 && $this->exists() && $oldid === null; - wfDebug( __METHOD__.': using parser cache: ' . ( $pcache ? 'yes' : 'no' ) . "\n" ); + wfDebug( __METHOD__.': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $wgUser->getOption( 'stubthreshold' ) ) { wfIncrStats( 'pcache_miss_stub' ); } $parserOutput = false; - if ( $pcache ) { - $parserOutput = $this->tryParserCache( $wgOut->parserOptions() ); + if ( $useParserCache ) { + $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() ); } if ( $parserOutput === false ) { // Cache miss; parse and output it. $rev = Revision::newFromTitle( $this->getTitle(), $oldid ); - return $this->outputFromWikitext( $rev->getText(), $pcache ); + return $this->getOutputFromWikitext( $rev->getText(), $useParserCache ); } else { return $parserOutput; } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 82743f39636..6174b36e492 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -158,6 +158,8 @@ $wgAutoloadLocalClasses = array( 'Pager' => 'includes/Pager.php', 'PasswordError' => 'includes/User.php', 'PatrolLog' => 'includes/PatrolLog.php', + 'PoolCounter' => 'includes/PoolCounter.php', + 'PoolCounter_Stub' => 'includes/PoolCounter.php', 'PostgresSearchResult' => 'includes/SearchPostgres.php', 'PostgresSearchResultSet' => 'includes/SearchPostgres.php', 'Preferences' => 'includes/Preferences.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 7890a533f70..1e3bfa63ac0 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3884,3 +3884,20 @@ $wgInvalidUsernameCharacters = '@'; * modify the user rights of those users via Special:UserRights */ $wgUserrightsInterwikiDelimiter = '@'; + +/** + * Configuration for processing pool control, for use in high-traffic wikis. + * An implementation is provided in the PoolCounter extension. + * + * This configuration array maps pool types to an associative array. The only + * defined key in the associative array is "class", which gives the class name. + * The remaining elements are passed through to the class as constructor + * parameters. Example: + * + * $wgPoolCounterConf = array( 'Article::view' => array( + * 'class' => 'PoolCounter_Client', + * ... any extension-specific options... + * ); + */ +$wgPoolCounterConf = null; + diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 36ca6ae7f39..a05867d6f58 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -715,12 +715,13 @@ class OutputPage { * @param Article $article * @param User $user * - * Now a wrapper around Article::tryParserCache() + * @deprecated * * @return bool True if successful, else false. */ public function tryParserCache( &$article ) { - $parserOutput = $article->tryParserCache( $this->parserOptions() ); + wfDeprecated( __METHOD__ ); + $parserOutput = ParserCache::singleton()->get( $article, $article->getParserOptions() ); if ($parserOutput !== false) { $this->addParserOutput( $parserOutput ); diff --git a/includes/PoolCounter.php b/includes/PoolCounter.php new file mode 100644 index 00000000000..acc2df772d0 --- /dev/null +++ b/includes/PoolCounter.php @@ -0,0 +1,64 @@ +acquire(); + if ( !$status->isOK() ) { + return $status; + } + if ( !empty( $status->value['overload'] ) ) { + # Overloaded. Try a dirty cache entry. + if ( $dirtyCallback ) { + if ( call_user_func( $dirtyCallback ) ) { + $this->release(); + return Status::newGood(); + } + } + + # Wait for a thread + $status = $this->wait(); + if ( !$status->isOK() ) { + $this->release(); + return $status; + } + } + # Call the main callback + call_user_func( $mainCallback ); + return $this->release(); + } +} + +class PoolCounter_Stub extends PoolCounter { + public function acquire() { + return Status::newGood(); + } + + public function release() { + return Status::newGood(); + } + + public function wait() { + return Status::newGood(); + } + + public function executeProtected( $mainCallback, $dirtyCallback = false ) { + call_user_func( $mainCallback ); + return Status::newGood(); + } +} + + diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php index d17214c3216..524d6be5b2e 100644 --- a/includes/parser/ParserCache.php +++ b/includes/parser/ParserCache.php @@ -7,7 +7,7 @@ class ParserCache { /** * Get an instance of this object */ - public static function &singleton() { + public static function singleton() { static $instance; if ( !isset( $instance ) ) { global $parserMemc; @@ -22,11 +22,11 @@ class ParserCache { * * @param object $memCached */ - function __construct( &$memCached ) { - $this->mMemc =& $memCached; + function __construct( $memCached ) { + $this->mMemc = $memCached; } - function getKey( &$article, $popts ) { + function getKey( $article, $popts ) { global $wgRequest; if( $popts instanceof User ) // It used to be getKey( &$article, &$user ) @@ -47,52 +47,55 @@ class ParserCache { return $key; } - function getETag( &$article, $popts ) { + function getETag( $article, $popts ) { return 'W/"' . $this->getKey($article, $popts) . "--" . $article->mTouched. '"'; } - function get( &$article, $popts ) { - global $wgCacheEpoch; - $fname = 'ParserCache::get'; - wfProfileIn( $fname ); - + function getDirty( $article, $popts ) { $key = $this->getKey( $article, $popts ); - wfDebug( "Trying parser cache $key\n" ); $value = $this->mMemc->get( $key ); - if ( is_object( $value ) ) { - wfDebug( "Found.\n" ); - # Delete if article has changed since the cache was made - $canCache = $article->checkTouched(); - $cacheTime = $value->getCacheTime(); - $touched = $article->mTouched; - if ( !$canCache || $value->expired( $touched ) ) { - if ( !$canCache ) { - wfIncrStats( "pcache_miss_invalid" ); - wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); - } else { - wfIncrStats( "pcache_miss_expired" ); - wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); - } - $this->mMemc->delete( $key ); - $value = false; - } else { - if ( isset( $value->mTimestamp ) ) { - $article->mTimestamp = $value->mTimestamp; - } - wfIncrStats( "pcache_hit" ); - } - } else { + return is_object( $value ) ? $value : false; + } + + function get( $article, $popts ) { + global $wgCacheEpoch; + wfProfileIn( __METHOD__ ); + + $value = $this->getDirty( $article, $popts ); + if ( !$value ) { wfDebug( "Parser cache miss.\n" ); wfIncrStats( "pcache_miss_absent" ); - $value = false; + wfProfileOut( __METHOD__ ); + return false; } - wfProfileOut( $fname ); + wfDebug( "Found.\n" ); + # Invalid if article has changed since the cache was made + $canCache = $article->checkTouched(); + $cacheTime = $value->getCacheTime(); + $touched = $article->mTouched; + if ( !$canCache || $value->expired( $touched ) ) { + if ( !$canCache ) { + wfIncrStats( "pcache_miss_invalid" ); + wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); + } else { + wfIncrStats( "pcache_miss_expired" ); + wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" ); + } + $value = false; + } else { + if ( isset( $value->mTimestamp ) ) { + $article->mTimestamp = $value->mTimestamp; + } + wfIncrStats( "pcache_hit" ); + } + + wfProfileOut( __METHOD__ ); return $value; } - function save( $parserOutput, &$article, $popts ){ + function save( $parserOutput, $article, $popts ){ global $wgParserCacheExpireTime; $key = $this->getKey( $article, $popts ); diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 5a3bd779d19..1aaf9fbf65a 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -846,6 +846,11 @@ XHTML id names. 'jumpto' => 'Jump to:', 'jumptonavigation' => 'navigation', 'jumptosearch' => 'search', +'view-pool-error' => 'Sorry, our servers are overloaded at the moment. +Too many people are trying to view this article. +Please try again in a minute or two. + +$1', # All link text and link target definitions of links into project namespace that get used by other message strings, with the exception of user group pages (see grouppage) and the disambiguation template definition (see disambiguations). 'aboutsite' => 'About {{SITENAME}}',