If PoolCounter acquisition would block and a stale ParserCache entry is available, deliver it immediately rather than waiting for the lock. This should avoid PoolCounter contention on heavily edited pages. * Add a fastStale pool option to toggle the feature. False by default but I'll set the default to true in a followup commit. * Add a $timeout parameter to PoolCounter::acquireForMe() and acquireForAnyone(). This requires a simultaneous update to the PoolCounter extension. * In the Redis implementation, use the requested timeout for blPop() but use the configured timeout for data structure cleanup and item expiry. * Add a boolean $fast parameter to fallback() which tells the subclass whether it is being called in the fast or slow mode. No extensions in CodeSearch extend PoolCounterWork directly so this should not cause a fatal. * Pass through the $fast parameter in PoolCounterWorkViaCallback * In PoolWorkArticleView, use the $fast flag to decide whether to check the ChronologyProtector touched timestamp. * Add $wgCdnMaxageStale by analogy with $wgCdnMaxageLagged, which controls the CC:s-maxage when sending a stale ParserOutput. * Fix the documented type of the timeout. It really should be a float, but locks.c will treat non-integers as zero. A simultaneous update to the PoolCounter extension is required. Bug: T250248 Change-Id: I1f410cd5d83588e584b6d27d2e106465f0fad23e
317 lines
9.1 KiB
PHP
317 lines
9.1 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
*/
|
|
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
use MediaWiki\Revision\RevisionRenderer;
|
|
use MediaWiki\Revision\RevisionStore;
|
|
use MediaWiki\Revision\SlotRecord;
|
|
|
|
class PoolWorkArticleView extends PoolCounterWork {
|
|
/** @var WikiPage */
|
|
private $page;
|
|
|
|
/** @var string */
|
|
private $cacheKey;
|
|
|
|
/** @var int */
|
|
private $revid;
|
|
|
|
/** @var ParserCache */
|
|
private $parserCache;
|
|
|
|
/** @var ParserOptions */
|
|
private $parserOptions;
|
|
|
|
/** @var RevisionRecord|null */
|
|
private $revision = null;
|
|
|
|
/** @var int */
|
|
private $audience;
|
|
|
|
/** @var RevisionStore */
|
|
private $revisionStore = null;
|
|
|
|
/** @var RevisionRenderer */
|
|
private $renderer = null;
|
|
|
|
/** @var ParserOutput|bool */
|
|
private $parserOutput = false;
|
|
|
|
/** @var bool */
|
|
private $isDirty = false;
|
|
|
|
/** @var bool */
|
|
private $isFast = false;
|
|
|
|
/** @var Status|bool */
|
|
private $error = false;
|
|
|
|
/**
|
|
* @param WikiPage $page
|
|
* @param ParserOptions $parserOptions ParserOptions to use for the parse
|
|
* @param int $revid ID of the revision being parsed.
|
|
* @param bool $useParserCache Whether to use the parser cache.
|
|
* operation.
|
|
* @param RevisionRecord|Content|string|null $revision Revision to render, or null to load it;
|
|
* may also be given as a wikitext string, or a Content object, for BC.
|
|
* @param int $audience One of the RevisionRecord audience constants
|
|
*/
|
|
public function __construct( WikiPage $page, ParserOptions $parserOptions,
|
|
$revid, $useParserCache, $revision = null, $audience = RevisionRecord::FOR_PUBLIC
|
|
) {
|
|
if ( is_string( $revision ) ) { // BC: very old style call
|
|
$revRecord = $page->getRevisionRecord();
|
|
$mainSlot = $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
|
|
$modelId = $mainSlot->getModel();
|
|
$format = $mainSlot->getFormat();
|
|
|
|
if ( $format === null ) {
|
|
$format = MediaWikiServices::getInstance()
|
|
->getContentHandlerFactory()
|
|
->getContentHandler( $modelId )
|
|
->getDefaultFormat();
|
|
}
|
|
|
|
$revision = ContentHandler::makeContent( $revision, $page->getTitle(), $modelId, $format );
|
|
}
|
|
|
|
if ( $revision instanceof Content ) { // BC: old style call
|
|
$content = $revision;
|
|
$revision = new MutableRevisionRecord( $page->getTitle() );
|
|
$revision->setId( $revid );
|
|
$revision->setPageId( $page->getId() );
|
|
$revision->setContent( SlotRecord::MAIN, $content );
|
|
}
|
|
|
|
if ( $revision ) {
|
|
// Check that the RevisionRecord matches $revid and $page, but still allow
|
|
// fake RevisionRecords coming from errors or hooks in Article to be rendered.
|
|
if ( $revision->getId() && $revision->getId() !== $revid ) {
|
|
throw new InvalidArgumentException( '$revid parameter mismatches $revision parameter' );
|
|
}
|
|
if ( $revision->getPageId()
|
|
&& $revision->getPageId() !== $page->getTitle()->getArticleID()
|
|
) {
|
|
throw new InvalidArgumentException( '$page parameter mismatches $revision parameter' );
|
|
}
|
|
}
|
|
|
|
// TODO: DI: inject services
|
|
$this->renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
|
|
$this->revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
|
|
$this->parserCache = MediaWikiServices::getInstance()->getParserCache();
|
|
|
|
$this->page = $page;
|
|
$this->revid = $revid;
|
|
$this->cacheable = $useParserCache;
|
|
$this->parserOptions = $parserOptions;
|
|
$this->revision = $revision;
|
|
$this->audience = $audience;
|
|
$this->cacheKey = $this->parserCache->getKey( $page, $parserOptions );
|
|
$keyPrefix = $this->cacheKey ?: ObjectCache::getLocalClusterInstance()->makeKey(
|
|
'articleview', 'missingcachekey'
|
|
);
|
|
|
|
parent::__construct( 'ArticleView', $keyPrefix . ':revid:' . $revid );
|
|
}
|
|
|
|
/**
|
|
* Get the ParserOutput from this object, or false in case of failure
|
|
*
|
|
* @return ParserOutput|bool
|
|
*/
|
|
public function getParserOutput() {
|
|
return $this->parserOutput;
|
|
}
|
|
|
|
/**
|
|
* Get whether the ParserOutput is a dirty one (i.e. expired)
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getIsDirty() {
|
|
return $this->isDirty;
|
|
}
|
|
|
|
/**
|
|
* Get whether the ParserOutput was retrieved in fast stale mode
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getIsFastStale() {
|
|
return $this->isFast;
|
|
}
|
|
|
|
/**
|
|
* Get a Status object in case of error or false otherwise
|
|
*
|
|
* @return Status|bool
|
|
*/
|
|
public function getError() {
|
|
return $this->error;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function doWork() {
|
|
global $wgUseFileCache;
|
|
|
|
// @todo several of the methods called on $this->page are not declared in Page, but present
|
|
// in WikiPage and delegated by Article.
|
|
|
|
$isCurrent = $this->revid === $this->page->getLatest();
|
|
|
|
// The current revision cannot be hidden so we can skip some checks.
|
|
$audience = $isCurrent ? RevisionRecord::RAW : $this->audience;
|
|
|
|
if ( $this->revision !== null ) {
|
|
$rev = $this->revision;
|
|
} elseif ( $isCurrent ) {
|
|
$rev = $this->page->getRevisionRecord();
|
|
} else {
|
|
$rev = $this->revisionStore->getRevisionByTitle( $this->page->getTitle(), $this->revid );
|
|
}
|
|
|
|
if ( !$rev ) {
|
|
// couldn't load
|
|
return false;
|
|
}
|
|
|
|
$renderedRevision = $this->renderer->getRenderedRevision(
|
|
$rev,
|
|
$this->parserOptions,
|
|
null,
|
|
[ 'audience' => $audience ]
|
|
);
|
|
|
|
if ( !$renderedRevision ) {
|
|
// audience check failed
|
|
return false;
|
|
}
|
|
|
|
// Reduce effects of race conditions for slow parses (T48014)
|
|
$cacheTime = wfTimestampNow();
|
|
|
|
$time = - microtime( true );
|
|
$this->parserOutput = $renderedRevision->getRevisionParserOutput();
|
|
$time += microtime( true );
|
|
|
|
// Timing hack
|
|
if ( $time > 3 ) {
|
|
// TODO: Use Parser's logger (once it has one)
|
|
$logger = MediaWiki\Logger\LoggerFactory::getInstance( 'slow-parse' );
|
|
$logger->info( '{time} {title}', [
|
|
'time' => number_format( $time, 2 ),
|
|
'title' => $this->page->getTitle()->getPrefixedDBkey(),
|
|
'ns' => $this->page->getTitle()->getNamespace(),
|
|
'trigger' => 'view',
|
|
] );
|
|
}
|
|
|
|
if ( $this->cacheable && $this->parserOutput->isCacheable() && $isCurrent ) {
|
|
$this->parserCache->save(
|
|
$this->parserOutput, $this->page, $this->parserOptions, $cacheTime, $this->revid );
|
|
}
|
|
|
|
// Make sure file cache is not used on uncacheable content.
|
|
// Output that has magic words in it can still use the parser cache
|
|
// (if enabled), though it will generally expire sooner.
|
|
if ( !$this->parserOutput->isCacheable() ) {
|
|
$wgUseFileCache = false;
|
|
}
|
|
|
|
if ( $isCurrent ) {
|
|
$this->page->triggerOpportunisticLinksUpdate( $this->parserOutput );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function getCachedWork() {
|
|
$this->parserOutput = $this->parserCache->get( $this->page, $this->parserOptions );
|
|
|
|
if ( $this->parserOutput === false ) {
|
|
wfDebug( __METHOD__ . ": parser cache miss" );
|
|
return false;
|
|
} else {
|
|
wfDebug( __METHOD__ . ": parser cache hit" );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param bool $fast Fast stale request
|
|
* @return bool
|
|
*/
|
|
public function fallback( $fast ) {
|
|
$this->parserOutput = $this->parserCache->getDirty( $this->page, $this->parserOptions );
|
|
|
|
$fastMsg = '';
|
|
if ( $this->parserOutput && $fast ) {
|
|
/* Check if the stale response is from before the last write to the
|
|
* DB by this user. Declining to return a stale response in this
|
|
* case ensures that the user will see their own edit after page
|
|
* save.
|
|
*
|
|
* Note that the CP touch time is the timestamp of the shutdown of
|
|
* the save request, so there is a bias towards avoiding fast stale
|
|
* responses of potentially several seconds.
|
|
*/
|
|
$lastWriteTime = MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
|
|
->getChronologyProtectorTouched();
|
|
$cacheTime = MWTimestamp::convert( TS_UNIX, $this->parserOutput->getCacheTime() );
|
|
if ( $lastWriteTime && $cacheTime <= $lastWriteTime ) {
|
|
wfDebugLog( 'dirty', "declining to send dirty output since cache time " .
|
|
$cacheTime . " is before last write time $lastWriteTime" );
|
|
// Forget this ParserOutput -- we will request it again if
|
|
// necessary in slow mode. There might be a newer entry
|
|
// available by that time.
|
|
$this->parserOutput = false;
|
|
return false;
|
|
}
|
|
$this->isFast = true;
|
|
$fastMsg = 'fast ';
|
|
}
|
|
|
|
if ( $this->parserOutput === false ) {
|
|
wfDebugLog( 'dirty', 'dirty missing' );
|
|
return false;
|
|
} else {
|
|
wfDebugLog( 'dirty', "{$fastMsg}dirty output {$this->cacheKey}" );
|
|
$this->isDirty = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Status $status
|
|
* @return bool
|
|
*/
|
|
public function error( $status ) {
|
|
$this->error = $status;
|
|
return false;
|
|
}
|
|
}
|