From 1353a8ba29512aeee8bf21115449751799023799 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Wed, 8 Jul 2009 08:12:35 +0000 Subject: [PATCH] Implemented the PoolCounter feature and did some general refactoring in the areas that it touched. * Renamed Article::outputFromWikitext() to Article::getOutputFromWikitext() * Factored out cascade protection updates * Removed recently-added Article::tryParserCache(): misnamed, can be done in one line of code in the caller. Deprecated OutputPage::tryParserCache(). * Made some functions public instead of protected when they could be useful from hooks. * In ParserCache, removed PHP 4-style ampersands In Article::view(): * Factored out robot policy logic, "redirected from" header, patrol footer, diff page, revdelete header, CSS/JS formatting, footer, namespace header, missing article error * Removed some variables, renamed some others, fixed incorrect use of empty() * Used the refactored footer section to do a couple of early returns and unindent a massive if(!$outputDone) block * Removed fantasy interpretation of $this->getContent()===false in comment * Don't try the parser cache when ArticleViewHeader specified $outputDone=true * Move timing hack to getOutputFromWikitext() * Stop using $wgOut->parserOptions() with save/restore nonsense every time you want to change something in it. This is meant to be OOP. * Don't overwrite the article text with an error message and then pretend to write it to the cache, that's confusing --- RELEASE-NOTES | 3 + includes/Article.php | 793 ++++++++++++++++++------------ includes/AutoLoader.php | 2 + includes/DefaultSettings.php | 17 + includes/OutputPage.php | 5 +- includes/PoolCounter.php | 64 +++ includes/parser/ParserCache.php | 75 +-- languages/messages/MessagesEn.php | 5 + 8 files changed, 615 insertions(+), 349 deletions(-) create mode 100644 includes/PoolCounter.php diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 731ba0b59e..c7033d13da 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 a7b75c5f73..4b231598a4 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() ); + + # 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; + } - # 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; + # Should the parser cache be used? + $useParserCache = $this->useParserCache( $oldid ); + wfDebug( 'Article::view using parser cache: ' . ($useParserCache ? 'yes' : 'no' ) . "\n" ); + if( $wgUser->getOption( 'stubthreshold' ) ) { + wfIncrStats( 'pcache_miss_stub' ); } - $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() ); + # For the main page, overwrite the element 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' ) ); + } - $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 ); + $wasRedirected = $this->showRedirectedFromHeader(); + $this->showNamespaceHeader(); - // 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(); + $outputDone = false; + wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) ); + + # Try the parser cache + if( !$outputDone && $useParserCache ) { + $parserOutput = $parserCache->get( $this, $parserOptions ); + if ( $parserOutput !== false ) { + wfDebug( __METHOD__.": showing parser cache contents\n" ); + $wgOut->addParserOutput( $parserOutput ); + // Ensure that UI elements requiring revision ID have + // the correct version information. + $wgOut->setRevisionId( $this->mLatest ); + $outputDone = true; + } + } + + if ( $outputDone ) { + $this->showViewFooter(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + return; + } + + $text = $this->getContent(); + if( $text === false || $this->getID() == 0 ) { + wfDebug( __METHOD__.": showing missing article\n" ); + $this->showMissingArticle(); + wfProfileOut( __METHOD__ ); + return; + } + + # Another whitelist check in case oldid is altering the title + if( !$this->mTitle->userCanRead() ) { + wfDebug( __METHOD__.": denied on secondary read check\n" ); + $wgOut->loginToUse(); + $wgOut->output(); + $wgOut->disable(); + wfProfileOut( __METHOD__ ); + return; + } + + # We're looking at an old revision + if( $oldid && !is_null( $this->mRevision ) ) { + $this->setOldSubtitle( $oldid ); + if ( !$this->showDeletedRevisionHeader() ) { + wfDebug( __METHOD__.": cannot view deleted revision\n" ); + wfProfileOut( __METHOD__ ); + return; } + + if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) { + $parserOutput = $parserCache->get( $this, $parserOptions ); + if ( $parserOutput ) { + wfDebug( __METHOD__.": showing parser cache for current rev permalink\n" ); + $wgOut->addParserOutput( $parserOutput ); + $this->showViewFooter(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + return; + } + } + } + + // Ensure that UI elements requiring revision ID have + // the correct version information. + $wgOut->setRevisionId( $this->getRevIdFetched() ); + + // Pages containing custom CSS or JavaScript get special treatment + if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + wfDebug( __METHOD__.": showing CSS/JS source\n" ); + $this->showCssOrJsPage(); + $outputDone = true; + } else if( $rt = Title::newFromRedirectArray( $text ) ) { + wfDebug( __METHOD__.": showing redirect=no page\n" ); + # Viewing a redirect page (e.g. with parameter redirect=no) + # Don't append the subtitle if this was an old revision + $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); + # Parse just to get categories, displaytitle, etc. + $parserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions ); + $wgOut->addParserOutputNoText( $parserOutput ); + $outputDone = true; + } + if ( $outputDone ) { + $this->showViewFooter(); + $this->viewUpdates(); wfProfileOut( __METHOD__ ); return; } + # Run the parse, protected by a pool counter + wfDebug( __METHOD__.": doing uncached parse\n" ); + $key = $parserCache->getKey( $this, $parserOptions ); + $poolCounter = PoolCounter::factory( 'Article::view', $key ); + $dirtyCallback = $useParserCache ? array( $this, 'tryDirtyCache' ) : false; + $status = $poolCounter->executeProtected( array( $this, 'doViewParse' ), $dirtyCallback ); + + if ( !$status->isOK() ) { + # Connection or timeout error + $this->showPoolError( $status ); + wfProfileOut( __METHOD__ ); + return; + } + + $this->showViewFooter(); + $this->viewUpdates(); + wfProfileOut( __METHOD__ ); + } + + /** + * Show a diff page according to current request variables. For use within + * Article::view() only, other callers should use the DifferenceEngine class. + */ + public function showDiffPage() { + global $wgOut, $wgRequest, $wgUser; + + $diff = $wgRequest->getVal( 'diff' ); + $rcid = $wgRequest->getVal( 'rcid' ); + $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); + $purge = $wgRequest->getVal( 'action' ) == 'purge'; + $htmldiff = $wgRequest->getVal( 'htmldiff' , false); + $unhide = $wgRequest->getInt('unhide') == 1; + $oldid = $this->getOldID(); + + $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(); + } + } + + /** + * Show a page view for a page formatted as CSS or JavaScript. To be called by + * Article::view() only. + * + * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these + * page views. + */ + public function showCssOrJsPage() { + global $wgOut; + $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 a <pre> and don't parse + $m = array(); + preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); + $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); + $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); + $wgOut->addHTML( "\n</pre>\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 ) { - # User/User_talk subpages are not modified. (bug 11443) + # 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->getBaseText() ) ) { - $wgOut->setRobotpolicy( 'noindex,nofollow' ); + if( $block->load( $this->mTitle->getText() ) ) { + return 'noindex,nofollow'; } } } - # Should the parser cache be used? - $pcache = $this->useParserCache( $oldid ); - wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" ); - if( $wgUser->getOption( 'stubthreshold' ) ) { - wfIncrStats( 'pcache_miss_stub' ); + 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; } + } - $wasRedirected = false; + /** + * 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( "<div class=\"mw-talkpageheader\">\n$1</div>", 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; + /** + * 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( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { + $wgOut->addWikiMsg('anontalkpagetext'); } - # 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 = "<div class='noarticletext'>\n$text\n</div>"; - if( !$this->hasViewableContent() ) { - // If there's no backing content, send a 404 Not Found - // for better machine handling of broken links. - $return404 = true; - } - } + # If we have been passed an &rcid= parameter, we want to give the user a + # chance to mark this new article as patrolled. + $this->showPatrolFooter(); - if( $return404 ) { - $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); - } + # Trackbacks + if( $wgUseTrackbacks ) { + $this->addTrackbacks(); + } + } - # Another whitelist check in case oldid is altering the title - if( !$this->mTitle->userCanRead() ) { - $wgOut->loginToUse(); - $wgOut->output(); - $wgOut->disable(); - wfProfileOut( __METHOD__ ); - return; - } + /** + * 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' ); - # For ?curid=x urls, disallow indexing - if( $wgRequest->getInt('curid') ) - $wgOut->setRobotPolicy( 'noindex,follow' ); + if( !$rcid || !$this->mTitle->exists() || !$this->mTitle->quickUserCan( 'patrol' ) ) { + return; + } - # 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( "<div class='mw-warning plainlinks'>\n$1</div>\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( "<div class='mw-warning plainlinks'>\n$1</div>\n", - array('rev-deleted-text-unhide',$link) ); - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - wfProfileOut( __METHOD__ ); - return; - // We are allowed to see... - } else { - $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\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; - } - } - } + $wgOut->addHTML( + "<div class='patrollink'>" . + wfMsgHtml( + 'markaspatrolledlink', + $sk->link( + $this->mTitle, + wfMsgHtml( 'markaspatrolledtext' ), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid + ), + array( 'known', 'noclasses' ) + ) + ) . + '</div>' + ); + } - // 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 a <pre> and don't parse - $m = array(); - preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m ); - $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); - $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); - $wgOut->addHTML( "\n</pre>\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())); - } + /** + * 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(); - if( !$this->isCurrent() ) { - $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting ); - } - } + # 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' ); } - /* title may have been set from the cache */ - $t = $wgOut->getPageTitle(); - if( empty( $t ) ) { - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - - # For the main page, overwrite the <title> element 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' ) ); - } + $text = "<div class='noarticletext'>\n$text\n</div>"; + 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 ); + } - # 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() ) ) { - $wgOut->addWikiMsg('anontalkpagetext'); - } + /** + * 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 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( - "<div class='patrollink'>" . - wfMsgHtml( - 'markaspatrolledlink', - $sk->link( - $this->mTitle, - wfMsgHtml( 'markaspatrolledtext' ), - array(), - array( - 'action' => 'markpatrolled', - 'rcid' => $rcid - ), - array( 'known', 'noclasses' ) - ) - ) . - '</div>' - ); + if( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + // Not deleted + return true; } - # Trackbacks - if( $wgUseTrackbacks ) { - $this->addTrackbacks(); + // If the user is not allowed to see it... + if( !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\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( "<div class='mw-warning plainlinks'>\n$1</div>\n", + array('rev-deleted-text-unhide',$link) ); + return false; + // We are allowed to see... + } else { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", + 'rev-deleted-text-view' ); + return true; } - - $this->viewUpdates(); - wfProfileOut( __METHOD__ ); } - protected function showLogs() { + /** + * 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( "<!-- parser cache is expired, sending anyway due to pool overload-->\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( + '<div class="errorbox">' . + $status->getWikiText( false, 'view-pool-error' ) . + '</div>' + ); + } + /** * 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 82743f3963..6174b36e49 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 7890a533f7..1e3bfa63ac 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 36ca6ae7f3..a05867d6f5 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 0000000000..acc2df772d --- /dev/null +++ b/includes/PoolCounter.php @@ -0,0 +1,64 @@ +<?php + +abstract class PoolCounter { + public function factory( $type, $key ) { + global $wgPoolCounterConf; + if ( !isset( $wgPoolCounterConf[$type] ) ) { + return new PoolCounter_Stub; + } + $conf = $wgPoolCounterConf[$type]; + $class = $conf['class']; + return new $class( $conf, $type, $key ); + } + + abstract public function acquire(); + abstract public function release(); + abstract public function wait(); + + public function executeProtected( $mainCallback, $dirtyCallback = false ) { + $status = $this->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 d17214c321..524d6be5b2 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" ); + wfProfileOut( __METHOD__ ); + return false; + } + + 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( $fname ); + 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 5a3bd779d1..1aaf9fbf65 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}}', -- 2.20.1