Merge "ParserOutputAccess: cache ouput for old revisions"
This commit is contained in:
commit
2be635539f
9 changed files with 512 additions and 51 deletions
|
|
@ -20,7 +20,6 @@ Some specific notes for MediaWiki 1.36 upgrades are below:
|
|||
For notes on 1.35.x and older releases, see HISTORY.
|
||||
|
||||
=== Configuration changes for system administrators in 1.36 ===
|
||||
|
||||
…
|
||||
|
||||
==== New configuration ====
|
||||
|
|
@ -29,6 +28,8 @@ For notes on 1.35.x and older releases, see HISTORY.
|
|||
configuration variable sets the maximum number of revisions of a page that
|
||||
will be checked against every new edit. Set this to 0 to disable the feature
|
||||
entirely.
|
||||
* $wgOldRevisionParserCacheExpireTime was added, to control caching of
|
||||
ParserOutput for old revisions (T244058).
|
||||
* …
|
||||
|
||||
==== Changed configuration ====
|
||||
|
|
|
|||
|
|
@ -1261,6 +1261,7 @@ $wgAutoloadLocalClasses = [
|
|||
'PoolCounterWorkViaCallback' => __DIR__ . '/includes/poolcounter/PoolCounterWorkViaCallback.php',
|
||||
'PoolWorkArticleView' => __DIR__ . '/includes/poolcounter/PoolWorkArticleView.php',
|
||||
'PoolWorkArticleViewCurrent' => __DIR__ . '/includes/poolcounter/PoolWorkArticleViewCurrent.php',
|
||||
'PoolWorkArticleViewOld' => __DIR__ . '/includes/poolcounter/PoolWorkArticleViewOld.php',
|
||||
'PopulateArchiveRevId' => __DIR__ . '/maintenance/populateArchiveRevId.php',
|
||||
'PopulateBacklinkNamespace' => __DIR__ . '/maintenance/populateBacklinkNamespace.php',
|
||||
'PopulateCategory' => __DIR__ . '/maintenance/populateCategory.php',
|
||||
|
|
|
|||
|
|
@ -2652,6 +2652,12 @@ $wgMainStash = 'db-replicated';
|
|||
*/
|
||||
$wgParserCacheExpireTime = 86400;
|
||||
|
||||
/**
|
||||
* The expiry time for the parser cache for old revisions, in seconds.
|
||||
* The default is 3600 (cache disabled).
|
||||
*/
|
||||
$wgOldRevisionParserCacheExpireTime = 60 * 60;
|
||||
|
||||
/**
|
||||
* The expiry time to use for session storage, in seconds.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -947,9 +947,12 @@ return [
|
|||
'ParserOutputAccess' => function ( MediaWikiServices $services ) : ParserOutputAccess {
|
||||
return new ParserOutputAccess(
|
||||
$services->getParserCache(),
|
||||
ObjectCache::getLocalClusterInstance(),
|
||||
$services->getMainConfig()->get( 'OldRevisionParserCacheExpireTime' ),
|
||||
$services->getRevisionRenderer(),
|
||||
$services->getStatsdDataFactory(),
|
||||
$services->getDBLoadBalancerFactory()
|
||||
$services->getDBLoadBalancerFactory(),
|
||||
$services->getJsonUnserializer()
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ class JsonUnserializer {
|
|||
$json = (array)$json;
|
||||
}
|
||||
|
||||
if ( !is_array( $json ) ) {
|
||||
throw new InvalidArgumentException(
|
||||
'Expected array, got ' . gettype( $json )
|
||||
);
|
||||
}
|
||||
|
||||
if ( !$this->canMakeNewFromValue( $json ) ) {
|
||||
throw new InvalidArgumentException( 'JSON did not have ' . self::TYPE_ANNOTATION );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@
|
|||
*/
|
||||
namespace MediaWiki\Page;
|
||||
|
||||
use BagOStuff;
|
||||
use IBufferingStatsdDataFactory;
|
||||
use InvalidArgumentException;
|
||||
use MediaWiki\Json\JsonUnserializer;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRenderer;
|
||||
use ParserCache;
|
||||
|
|
@ -30,6 +32,7 @@ use ParserOptions;
|
|||
use ParserOutput;
|
||||
use PoolWorkArticleView;
|
||||
use PoolWorkArticleViewCurrent;
|
||||
use PoolWorkArticleViewOld;
|
||||
use Status;
|
||||
use Wikimedia\Rdbms\ILBFactory;
|
||||
use WikiPage;
|
||||
|
|
@ -72,9 +75,26 @@ class ParserOutputAccess {
|
|||
*/
|
||||
public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
|
||||
|
||||
/** @var string Do not read or write any cache */
|
||||
private const CACHE_NONE = 'none';
|
||||
|
||||
/** @var string Use primary cache */
|
||||
private const CACHE_PRIMARY = 'primary';
|
||||
|
||||
/** @var string Use secondary cache */
|
||||
private const CACHE_SECONDARY = 'secondary';
|
||||
|
||||
/** @var ParserCache */
|
||||
private $primaryCache;
|
||||
|
||||
/**
|
||||
* @var BagOStuff
|
||||
*/
|
||||
private $secondaryCache;
|
||||
|
||||
/** @var int */
|
||||
private $secondaryCacheExpiry;
|
||||
|
||||
/** @var RevisionRenderer */
|
||||
private $revisionRenderer;
|
||||
|
||||
|
|
@ -84,22 +104,34 @@ class ParserOutputAccess {
|
|||
/** @var ILBFactory */
|
||||
private $lbFactory;
|
||||
|
||||
/** @var JsonUnserializer */
|
||||
private $jsonUnserializer;
|
||||
|
||||
/**
|
||||
* @param ParserCache $primaryCache
|
||||
* @param BagOStuff $secondaryCache
|
||||
* @param int $secondaryCacheExpiry
|
||||
* @param RevisionRenderer $revisionRenderer
|
||||
* @param IBufferingStatsdDataFactory $statsDataFactory
|
||||
* @param ILBFactory $lbFactory
|
||||
* @param JsonUnserializer $jsonUnserializer
|
||||
*/
|
||||
public function __construct(
|
||||
ParserCache $primaryCache,
|
||||
BagOStuff $secondaryCache,
|
||||
int $secondaryCacheExpiry,
|
||||
RevisionRenderer $revisionRenderer,
|
||||
IBufferingStatsdDataFactory $statsDataFactory,
|
||||
ILBFactory $lbFactory
|
||||
ILBFactory $lbFactory,
|
||||
JsonUnserializer $jsonUnserializer
|
||||
) {
|
||||
$this->primaryCache = $primaryCache;
|
||||
$this->secondaryCache = $secondaryCache;
|
||||
$this->secondaryCacheExpiry = $secondaryCacheExpiry;
|
||||
$this->revisionRenderer = $revisionRenderer;
|
||||
$this->statsDataFactory = $statsDataFactory;
|
||||
$this->lbFactory = $lbFactory;
|
||||
$this->jsonUnserializer = $jsonUnserializer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,7 +141,7 @@ class ParserOutputAccess {
|
|||
* @param ParserOptions $parserOptions ParserOptions to check
|
||||
* @param RevisionRecord|null $rev
|
||||
*
|
||||
* @return bool
|
||||
* @return string One of the CACHE_XXX constants.
|
||||
*/
|
||||
private function shouldUseCache(
|
||||
WikiPage $page,
|
||||
|
|
@ -118,17 +150,34 @@ class ParserOutputAccess {
|
|||
) {
|
||||
if ( $rev && !$rev->getId() ) {
|
||||
// The revision isn't from the database, so the output can't safely be cached.
|
||||
return false;
|
||||
return self::CACHE_NONE;
|
||||
}
|
||||
|
||||
// NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
|
||||
// NOTE: when we allow caching of old revisions in the future,
|
||||
// we must not allow caching of deleted revisions.
|
||||
$oldId = $rev ? $rev->getId() : 0;
|
||||
return $parserOptions->getStubThreshold() == 0
|
||||
&& $page->exists()
|
||||
&& ( $oldId === null || $oldId === 0 || $oldId === $page->getLatest() )
|
||||
&& $page->getContentHandler()->isParserCacheSupported();
|
||||
|
||||
if ( $parserOptions->getStubThreshold() !== 0
|
||||
|| !$page->exists()
|
||||
|| !$page->getContentHandler()->isParserCacheSupported()
|
||||
) {
|
||||
return self::CACHE_NONE;
|
||||
}
|
||||
|
||||
if ( !$rev || $rev->getId() === $page->getLatest() ) {
|
||||
// current revision
|
||||
return self::CACHE_PRIMARY;
|
||||
}
|
||||
|
||||
if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
|
||||
// deleted/suppressed revision
|
||||
return self::CACHE_NONE;
|
||||
}
|
||||
|
||||
if ( $this->secondaryCacheExpiry > 0 ) {
|
||||
return self::CACHE_SECONDARY;
|
||||
}
|
||||
return self::CACHE_NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,11 +201,17 @@ class ParserOutputAccess {
|
|||
return null;
|
||||
}
|
||||
|
||||
if ( !$this->shouldUseCache( $page, $parserOptions, $revision ) ) {
|
||||
return null;
|
||||
}
|
||||
$useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
|
||||
|
||||
$output = $this->primaryCache->get( $page, $parserOptions );
|
||||
if ( $useCache === self::CACHE_PRIMARY ) {
|
||||
$output = $this->primaryCache->get( $page, $parserOptions );
|
||||
} elseif ( $useCache === self::CACHE_SECONDARY ) {
|
||||
$cacheKey = $this->getSecondaryCacheKey( $parserOptions, $revision );
|
||||
$json = $this->secondaryCache->get( $cacheKey );
|
||||
$output = $json ? $this->jsonUnserializer->unserialize( $json ) : null;
|
||||
} else {
|
||||
$output = null;
|
||||
}
|
||||
|
||||
return $output ?: null; // convert false to null
|
||||
}
|
||||
|
|
@ -295,6 +350,18 @@ class ParserOutputAccess {
|
|||
return null;
|
||||
}
|
||||
|
||||
private function getSecondaryCacheKey(
|
||||
ParserOptions $parserOptions,
|
||||
?RevisionRecord $revision
|
||||
) {
|
||||
// NOTE: For now, split the cache on all options. Eventually, we may implement a
|
||||
// two-tiered system like in ParserCache, or generalize ParserCache itself
|
||||
// to cover old revisions.
|
||||
$revId = $revision ? $revision->getId() : 0;
|
||||
$hash = $parserOptions->optionsHash( ParserOptions::allCacheVaryingOptions() );
|
||||
return $this->secondaryCache->makeKey( __CLASS__, $hash, self::CACHE_SECONDARY, $revId );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WikiPage $page
|
||||
* @param ParserOptions $parserOptions
|
||||
|
|
@ -310,40 +377,53 @@ class ParserOutputAccess {
|
|||
int $options
|
||||
): PoolWorkArticleView {
|
||||
if ( $options & self::OPT_NO_UPDATE_CACHE ) {
|
||||
$useCache = false;
|
||||
$useCache = self::CACHE_NONE;
|
||||
} else {
|
||||
$useCache = $this->shouldUseCache( $page, $parserOptions, $revision );
|
||||
}
|
||||
|
||||
$parserCacheMetadata = $this->primaryCache->getMetadata( $page );
|
||||
$cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions,
|
||||
$parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
|
||||
);
|
||||
switch ( $useCache ) {
|
||||
case self::CACHE_PRIMARY:
|
||||
$parserCacheMetadata = $this->primaryCache->getMetadata( $page );
|
||||
$cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions,
|
||||
$parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
|
||||
);
|
||||
|
||||
$workKey = $cacheKey . ':revid:' . $revision->getId();
|
||||
$workKey = $cacheKey . ':revid:' . $revision->getId();
|
||||
|
||||
if ( $useCache ) {
|
||||
$workKey .= ':current';
|
||||
$work = new PoolWorkArticleViewCurrent(
|
||||
$workKey,
|
||||
$page,
|
||||
$revision,
|
||||
$parserOptions,
|
||||
$this->revisionRenderer,
|
||||
$this->primaryCache,
|
||||
$this->lbFactory
|
||||
);
|
||||
} else {
|
||||
$workKey .= ':uncached';
|
||||
$work = new PoolWorkArticleView(
|
||||
$workKey,
|
||||
$revision,
|
||||
$parserOptions,
|
||||
$this->revisionRenderer
|
||||
);
|
||||
return new PoolWorkArticleViewCurrent(
|
||||
$workKey,
|
||||
$page,
|
||||
$revision,
|
||||
$parserOptions,
|
||||
$this->revisionRenderer,
|
||||
$this->primaryCache,
|
||||
$this->lbFactory
|
||||
);
|
||||
|
||||
case $useCache == self::CACHE_SECONDARY:
|
||||
$cacheKey = $this->getSecondaryCacheKey( $parserOptions, $revision );
|
||||
return new PoolWorkArticleViewOld(
|
||||
$cacheKey,
|
||||
$this->secondaryCacheExpiry,
|
||||
$this->secondaryCache,
|
||||
$revision,
|
||||
$parserOptions,
|
||||
$this->revisionRenderer,
|
||||
$this->jsonUnserializer
|
||||
);
|
||||
|
||||
default:
|
||||
$workKey = $this->getSecondaryCacheKey( $parserOptions, $revision ) . ':uncached';
|
||||
return new PoolWorkArticleView(
|
||||
$workKey,
|
||||
$revision,
|
||||
$parserOptions,
|
||||
$this->revisionRenderer
|
||||
);
|
||||
}
|
||||
|
||||
return $work;
|
||||
// unreachable
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
171
includes/poolcounter/PoolWorkArticleViewOld.php
Normal file
171
includes/poolcounter/PoolWorkArticleViewOld.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?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\Json\JsonUnserializer;
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRenderer;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
||||
/**
|
||||
* PoolWorkArticleView for an old revision of a page, using a simple cache.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PoolWorkArticleViewOld extends PoolWorkArticleView {
|
||||
/** @var int */
|
||||
private $cacheExpiry;
|
||||
|
||||
/** @var BagOStuff */
|
||||
private $cache;
|
||||
|
||||
/** @var string */
|
||||
private $cacheKey;
|
||||
|
||||
/** @var JsonUnserializer */
|
||||
private $jsonUnserializer;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* @param string $cacheKey Key for the ParserOutput to use in $cache.
|
||||
* Also used as the PoolCounter key.
|
||||
* @param int $cacheExpiry Expiry for ParserOutput in $cache.
|
||||
* @param BagOStuff $cache The cache to store ParserOutput in.
|
||||
* @param RevisionRecord $revision Revision to render
|
||||
* @param ParserOptions $parserOptions ParserOptions to use for the parse
|
||||
* @param RevisionRenderer $revisionRenderer
|
||||
* @param JsonUnserializer $jsonUnserializer
|
||||
*/
|
||||
public function __construct(
|
||||
string $cacheKey,
|
||||
int $cacheExpiry,
|
||||
BagOStuff $cache,
|
||||
RevisionRecord $revision,
|
||||
ParserOptions $parserOptions,
|
||||
RevisionRenderer $revisionRenderer,
|
||||
JsonUnserializer $jsonUnserializer
|
||||
) {
|
||||
Assert::parameter( $cacheExpiry > 0, '$cacheExpiry', 'must be greater than zero' );
|
||||
|
||||
parent::__construct( $cacheKey, $revision, $parserOptions, $revisionRenderer );
|
||||
|
||||
$this->cacheKey = $cacheKey;
|
||||
$this->cacheExpiry = $cacheExpiry;
|
||||
$this->cache = $cache;
|
||||
$this->jsonUnserializer = $jsonUnserializer;
|
||||
|
||||
// TODO: inject logger into all PoolWorkArticleView subclasses via ParserOutputAccess
|
||||
$this->logger = LoggerFactory::getInstance( 'PoolWorkArticleView' );
|
||||
|
||||
$this->cacheable = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ParserOutput $output
|
||||
* @param string $cacheTime
|
||||
*/
|
||||
protected function saveInCache( ParserOutput $output, string $cacheTime ) {
|
||||
$json = $this->encodeAsJson( $output );
|
||||
if ( $json === null ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cache->set( $this->cacheKey, $json, $this->cacheExpiry );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getCachedWork() {
|
||||
$json = $this->cache->get( $this->cacheKey );
|
||||
|
||||
if ( $json === false ) {
|
||||
$this->logger->debug( __METHOD__ . ": output cache miss" );
|
||||
return false;
|
||||
} else {
|
||||
$this->logger->debug( __METHOD__ . ": output cache hit" );
|
||||
}
|
||||
|
||||
$output = $this->restoreFromJson( $json );
|
||||
|
||||
// Note: if $output is null, $this->parserOutput remains false, not null.
|
||||
if ( $output === null ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->parserOutput = $output;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $json
|
||||
*
|
||||
* @return ParserOutput|null
|
||||
*/
|
||||
private function restoreFromJson( string $json ) {
|
||||
try {
|
||||
/** @var ParserOutput $obj */
|
||||
$obj = $this->jsonUnserializer->unserialize( $json, ParserOutput::class );
|
||||
return $obj;
|
||||
} catch ( InvalidArgumentException $e ) {
|
||||
$this->logger->error( "Unable to unserialize JSON", [
|
||||
'cache_key' => $this->cacheKey,
|
||||
'message' => $e->getMessage()
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ParserOutput $output
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function encodeAsJson( ParserOutput $output ) {
|
||||
$data = $output->jsonSerialize();
|
||||
$json = FormatJson::encode( $data, false, FormatJson::ALL_OK );
|
||||
if ( !$json ) {
|
||||
$this->logger->error( "JSON encoding failed", [
|
||||
'cache_key' => $this->cacheKey,
|
||||
'json_error' => json_last_error(),
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detect if the array contained any properties non-serializable
|
||||
// to json. We will not be able to deserialize the value correctly
|
||||
// anyway, so return null. This is done after calling FormatJson::encode
|
||||
// to avoid walking over circular structures.
|
||||
$unserializablePath = FormatJson::detectNonSerializableData( $data, true );
|
||||
if ( $unserializablePath ) {
|
||||
$this->logger->error( 'Non-serializable {class} property set', [
|
||||
'class' => get_class( $output ),
|
||||
'cache_key' => $this->cacheKey,
|
||||
'path' => $unserializablePath,
|
||||
] );
|
||||
return null;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ use MediaWiki\Json\JsonUnserializer;
|
|||
use MediaWiki\Page\ParserOutputAccess;
|
||||
use MediaWiki\Revision\MutableRevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRenderer;
|
||||
use MediaWiki\Revision\SlotRecord;
|
||||
use MediaWiki\Storage\RevisionStore;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
|
@ -24,7 +25,9 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|||
$value = $value->getText();
|
||||
}
|
||||
|
||||
$html = trim( preg_replace( '/<!--.*?-->/s', '', $value ) );
|
||||
$html = preg_replace( '/<!--.*?-->/s', '', $value );
|
||||
$html = trim( preg_replace( '/[\r\n]{2,}/s', "\n", $html ) );
|
||||
$html = trim( preg_replace( '/\s{2,}/s', ' ', $html ) );
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
|
@ -76,18 +79,52 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
/**
|
||||
* @param ParserCache|null $parserCache
|
||||
* @param BagOStuff|null $secondaryCache
|
||||
* @param int $secondaryExpiry
|
||||
*
|
||||
* @param int|bool $maxRenderCalls
|
||||
*
|
||||
* @return ParserOutputAccess
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getParserOutputAccessWithCache( $parserCache = null ) {
|
||||
private function getParserOutputAccessWithCache(
|
||||
$parserCache = null,
|
||||
$secondaryCache = null,
|
||||
$secondaryExpiry = 3600,
|
||||
$maxRenderCalls = false
|
||||
) {
|
||||
if ( !$parserCache ) {
|
||||
$parserCache = $this->getParserCache( new HashBagOStuff() );
|
||||
}
|
||||
|
||||
if ( !$secondaryCache ) {
|
||||
$secondaryCache = new HashBagOStuff();
|
||||
}
|
||||
|
||||
$revRenderer = $this->getServiceContainer()->getRevisionRenderer();
|
||||
$lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
|
||||
$stats = new NullStatsdDataFactory();
|
||||
return new ParserOutputAccess( $parserCache, $revRenderer, $stats, $lbFactory );
|
||||
$jsonUnserializer = new JsonUnserializer();
|
||||
|
||||
if ( $maxRenderCalls ) {
|
||||
$realRevRenderer = $revRenderer;
|
||||
$revRenderer =
|
||||
$this->createNoOpMock( RevisionRenderer::class, [ 'getRenderedRevision' ] );
|
||||
|
||||
$revRenderer->expects( $this->atMost( $maxRenderCalls ) )
|
||||
->method( 'getRenderedRevision' )
|
||||
->willReturnCallback( [ $realRevRenderer, 'getRenderedRevision' ] );
|
||||
}
|
||||
|
||||
return new ParserOutputAccess(
|
||||
$parserCache,
|
||||
$secondaryCache,
|
||||
$secondaryExpiry,
|
||||
$revRenderer,
|
||||
$stats,
|
||||
$lbFactory,
|
||||
$jsonUnserializer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -95,7 +132,7 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|||
*/
|
||||
private function getParserOutputAccessNoCache() {
|
||||
$cache = $this->getParserCache( new EmptyBagOStuff() );
|
||||
return $this->getParserOutputAccessWithCache( $cache );
|
||||
return $this->getParserOutputAccessWithCache( $cache, new EmptyBagOStuff() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -139,18 +176,18 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|||
* Tests that cached output in the ParserCache will be used for the latest revision.
|
||||
*/
|
||||
public function testLatestRevisionUseCached() {
|
||||
$parserCache = $this->getParserCache( new HashBagOStuff() );
|
||||
$access = $this->getParserOutputAccessWithCache( $parserCache );
|
||||
// Allow only one render call, use default caches
|
||||
$access = $this->getParserOutputAccessWithCache( null, null, 3600, 1 );
|
||||
|
||||
$parserOptions = $this->getParserOptions();
|
||||
$page = $this->getNonexistingTestPage( __METHOD__ );
|
||||
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
||||
|
||||
$expectedOutput = new ParserOutput( 'Cached Text' );
|
||||
$parserCache->save( $expectedOutput, $page, $parserOptions );
|
||||
$access->getParserOutput( $page, $parserOptions );
|
||||
|
||||
// The second call should use cached output
|
||||
$status = $access->getParserOutput( $page, $parserOptions );
|
||||
$this->assertSameHtml( $expectedOutput, $status );
|
||||
$this->assertContainsHtml( 'Hello <i>World</i>!', $status );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -400,14 +437,87 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|||
* Tests that output for an old revision is fetched from the secondary parser cache if possible.
|
||||
*/
|
||||
public function testOldRevisionUseCached() {
|
||||
$this->markTestSkipped( 'Caching not yet implemented for old revisions' );
|
||||
// Allow only one render call, use default caches
|
||||
$access = $this->getParserOutputAccessWithCache( null, null, 3600, 1 );
|
||||
|
||||
$parserOptions = $this->getParserOptions();
|
||||
$page = $this->getNonexistingTestPage( __METHOD__ );
|
||||
$this->editPage( $page, 'First' );
|
||||
$oldRev = $page->getRevisionRecord();
|
||||
|
||||
$this->editPage( $page, 'Second' );
|
||||
|
||||
$firstStatus = $access->getParserOutput( $page, $parserOptions, $oldRev );
|
||||
|
||||
// The second call should use cached output
|
||||
$secondStatus = $access->getParserOutput( $page, $parserOptions, $oldRev );
|
||||
$this->assertSameHtml( $firstStatus, $secondStatus );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that output for an old revision is fetched from the secondary parser cache if possible.
|
||||
*/
|
||||
public function testOldRevisionDisableCached() {
|
||||
// Use default caches, but expiry 0 for the secondary cache
|
||||
$access = $this->getParserOutputAccessWithCache( null, null, 0 );
|
||||
|
||||
$parserOptions = $this->getParserOptions();
|
||||
$page = $this->getNonexistingTestPage( __METHOD__ );
|
||||
$this->editPage( $page, 'First' );
|
||||
$oldRev = $page->getRevisionRecord();
|
||||
|
||||
$this->editPage( $page, 'Second' );
|
||||
$access->getParserOutput( $page, $parserOptions, $oldRev );
|
||||
|
||||
// Should not be cached!
|
||||
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions, $oldRev );
|
||||
$this->assertNull( $cachedOutput );
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the secondary cache for output for old revisions is split on parser options.
|
||||
*/
|
||||
public function testOldRevisionCacheSplit() {
|
||||
$this->markTestSkipped( 'Caching not yet implemented for old revisions' );
|
||||
$access = $this->getParserOutputAccessWithCache();
|
||||
|
||||
$frenchOptions = ParserOptions::newCanonical( 'canonical' );
|
||||
$frenchOptions->setUserLang( 'fr' );
|
||||
|
||||
$tongaOptions = ParserOptions::newCanonical( 'canonical' );
|
||||
$tongaOptions->setUserLang( 'to' );
|
||||
|
||||
$page = $this->getNonexistingTestPage( __METHOD__ );
|
||||
$this->editPage( $page, 'Test {{int:ok}}!' );
|
||||
$oldRev = $page->getRevisionRecord();
|
||||
|
||||
$this->editPage( $page, 'Latest Test' );
|
||||
|
||||
$frenchResult = $access->getParserOutput( $page, $frenchOptions, $oldRev );
|
||||
$this->assertContainsHtml( 'Test', $frenchResult );
|
||||
|
||||
// sanity check that French output was cached
|
||||
$cachedFrenchOutput =
|
||||
$access->getCachedParserOutput( $page, $frenchOptions, $oldRev );
|
||||
$this->assertNotNull( $cachedFrenchOutput, 'French output should be in the cache' );
|
||||
|
||||
// check that we don't get the French output when asking for Tonga
|
||||
$cachedTongaOutput =
|
||||
$access->getCachedParserOutput( $page, $tongaOptions, $oldRev );
|
||||
$this->assertNull( $cachedTongaOutput, 'Tonga output should not be in the cache yet' );
|
||||
|
||||
// check that we can generate the Tonga output, and it's different from French
|
||||
$tongaResult = $access->getParserOutput( $page, $tongaOptions, $oldRev );
|
||||
$this->assertContainsHtml( 'Test', $tongaResult );
|
||||
$this->assertNotSameHtml(
|
||||
$frenchResult,
|
||||
$tongaResult,
|
||||
'Tonga output should be different from French'
|
||||
);
|
||||
|
||||
// check that the Tonga output is cached
|
||||
$cachedTongaOutput =
|
||||
$access->getCachedParserOutput( $page, $tongaOptions, $oldRev );
|
||||
$this->assertNotNull( $cachedTongaOutput, 'Tonga output should be in the cache' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\Json\JsonUnserializer;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
||||
/**
|
||||
* @covers PoolWorkArticleViewOld
|
||||
* @group Database
|
||||
*/
|
||||
class PoolWorkArticleViewOldTest extends PoolWorkArticleViewTest {
|
||||
|
||||
/** @var BagOStuff */
|
||||
private $cache = null;
|
||||
|
||||
/**
|
||||
* @param WikiPage $page
|
||||
* @param RevisionRecord|null $rev
|
||||
* @param ParserOptions|null $options
|
||||
*
|
||||
* @return PoolWorkArticleView
|
||||
*/
|
||||
protected function newPoolWorkArticleView(
|
||||
WikiPage $page,
|
||||
RevisionRecord $rev = null,
|
||||
$options = null
|
||||
) {
|
||||
if ( !$options ) {
|
||||
$options = ParserOptions::newCanonical( 'canonical' );
|
||||
}
|
||||
|
||||
if ( !$rev ) {
|
||||
$rev = $page->getRevisionRecord();
|
||||
}
|
||||
|
||||
if ( !$this->cache ) {
|
||||
$this->installCache();
|
||||
}
|
||||
|
||||
$renderer = $this->getServiceContainer()->getRevisionRenderer();
|
||||
$unserializer = new JsonUnserializer();
|
||||
|
||||
return new PoolWorkArticleViewOld(
|
||||
'test:' . $rev->getId(),
|
||||
60 * 60,
|
||||
$this->cache,
|
||||
$rev,
|
||||
$options,
|
||||
$renderer,
|
||||
$unserializer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BagOStuff $bag
|
||||
*
|
||||
* @return BagOStuff
|
||||
*/
|
||||
private function installCache( $bag = null ) {
|
||||
$this->cache = $bag ?: new HashBagOStuff();
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function testUpdateCachedOutput() {
|
||||
$page = $this->getExistingTestPage( __METHOD__ );
|
||||
|
||||
$cache = $this->installCache();
|
||||
|
||||
$work = $this->newPoolWorkArticleView( $page );
|
||||
$this->assertTrue( $work->execute() );
|
||||
|
||||
$cacheKey = 'test:' . $page->getLatest();
|
||||
|
||||
$cachedJson = $cache->get( $cacheKey );
|
||||
$this->assertIsString( $cachedJson );
|
||||
|
||||
$jsonUnserialiezr = new JsonUnserializer();
|
||||
$cachedOutput = $jsonUnserialiezr->unserialize( $cachedJson );
|
||||
$this->assertNotEmpty( $cachedOutput );
|
||||
|
||||
$this->assertSame( $work->getParserOutput()->getText(), $cachedOutput->getText() );
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue