HtmlOutputRendererHelper: allow parser cache to be disabled.

This is needed so we can ramp up parser cache writes in a controlled
manner.

Bug: T322672
Change-Id: I7d97c9e2d4009029dc64f9c0a369f68098185520
This commit is contained in:
daniel 2022-11-24 20:58:56 +01:00 committed by D3r1ck01
parent 2839a0b9d3
commit c6a0d433ec
4 changed files with 163 additions and 20 deletions

View file

@ -107,6 +107,14 @@ class HtmlOutputRendererHelper {
/** @var string|null */
private $targetLanguageCode;
/**
* Flags to be passed as $options to ParsoidOutputAccess::getParserOutput,
* to control parser cache access.
*
* @var int Use ParsoidOutputAccess::OPT_*
*/
private $parsoidOutputAccessOptions = 0;
/**
* @param ParsoidOutputStash $parsoidOutputStash
* @param StatsdDataFactoryInterface $statsDataFactory
@ -141,6 +149,18 @@ class HtmlOutputRendererHelper {
$this->flavor = $flavor;
}
/**
* Controls how the parser cache is used.
*
* @param bool $read Whether we should look for cached output before parsing
* @param bool $write Whether we should cache output after parsing
*/
public function setUseParserCache( bool $read, bool $write ) {
$this->parsoidOutputAccessOptions =
( $read ? 0 : ParsoidOutputAccess::OPT_FORCE_PARSE ) |
( $write ? 0 : ParsoidOutputAccess::OPT_NO_UPDATE_CACHE );
}
/**
* Determine whether stashing should be applied.
*
@ -386,7 +406,8 @@ class HtmlOutputRendererHelper {
$status = $this->parsoidOutputAccess->getParserOutput(
$this->page,
$parserOptions,
$this->revisionOrId
$this->revisionOrId,
$this->parsoidOutputAccessOptions
);
} else {
$status = $this->parsoidOutputAccess->parse(

View file

@ -69,6 +69,11 @@ class ParsoidOutputAccess {
/** @var int Do not check the cache before parsing (force parse) */
public const OPT_FORCE_PARSE = 1;
/**
* @var int Do not update the cache after parsing.
*/
public const OPT_NO_UPDATE_CACHE = 2;
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::ParsoidCacheConfig,
MainConfigNames::ParsoidSettings,
@ -206,10 +211,14 @@ class ParsoidOutputAccess {
}
$startTime = microtime( true );
$status = $this->parse( $page, $parserOpts, [ 'pageBundle' => true ], $revision );
$status = $this->parse( $page, $parserOpts, [], $revision );
$time = microtime( true ) - $startTime;
if ( $status->isOK() ) {
if ( !$status->isOK() ) {
$this->stats->increment( $statsKey . '.save.notok' );
} elseif ( $options & self::OPT_NO_UPDATE_CACHE ) {
$this->stats->increment( $statsKey . '.save.disabled' );
} else {
if ( $time > $this->parsoidCacheConfig->get( 'CacheThresholdTime' ) ) {
$parserOutput = $status->getValue();
$now = $parserOutput->getCacheTime();
@ -223,8 +232,6 @@ class ParsoidOutputAccess {
} else {
$this->stats->increment( $statsKey . '.save.skipfast' );
}
} else {
$this->stats->increment( $statsKey . '.save.notok' );
}
return $status;

View file

@ -16,9 +16,11 @@ use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\PageIdentityValue;
use MediaWiki\Page\PageLookup;
use MediaWiki\Page\PageRecord;
use MediaWiki\Parser\ParserCacheFactory;
use MediaWiki\Parser\Parsoid\PageBundleParserOutputConverter;
use MediaWiki\Parser\Parsoid\ParsoidOutputAccess;
use MediaWiki\Parser\Parsoid\ParsoidRenderID;
use MediaWiki\Parser\RevisionOutputCache;
use MediaWiki\Rest\Handler\HtmlOutputRendererHelper;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Rest\ResponseInterface;
@ -28,6 +30,7 @@ use MediaWiki\Revision\SlotRecord;
use MediaWikiIntegrationTestCase;
use MWTimestamp;
use NullStatsdDataFactory;
use ParserCache;
use ParserOptions;
use ParserOutput;
use PHPUnit\Framework\MockObject\MockObject;
@ -36,6 +39,7 @@ use Status;
use User;
use Wikimedia\Message\MessageValue;
use Wikimedia\Parsoid\Core\ClientError;
use Wikimedia\Parsoid\Core\PageBundle;
use Wikimedia\Parsoid\Core\ResourceLimitExceededException;
use Wikimedia\Parsoid\Parsoid;
use Wikimedia\TestingAccessWrapper;
@ -544,6 +548,57 @@ class HtmlOutputRendererHelperTest extends MediaWikiIntegrationTestCase {
];
}
private function newRealParsoidOutputAccess( $overrides = [] ) {
if ( isset( $overrides['parsoid'] ) ) {
$parsoid = $overrides['parsoid'];
} else {
$parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] );
$parsoid->method( 'wikitext2html' )
->willReturn( new PageBundle( 'This is HTML' ) );
}
if ( isset( $overrides['parserCache'] ) ) {
$parserCache = $overrides['parserCache'];
} else {
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'get', 'save' ] );
$parserCache->method( 'get' )->willReturn( false );
$parserCache->method( 'save' )->willReturn( null );
}
if ( isset( $overrides['revisionCache'] ) ) {
$revisionCache = $overrides['revisionCache'];
} else {
$revisionCache = $this->createNoOpMock( RevisionOutputCache::class, [ 'get', 'save' ] );
$revisionCache->method( 'get' )->willReturn( false );
$revisionCache->method( 'save' )->willReturn( null );
}
$parserCacheFactory = $this->createNoOpMock(
ParserCacheFactory::class,
[ 'getParserCache', 'getRevisionOutputCache' ]
);
$parserCacheFactory->method( 'getParserCache' )->willReturn( $parserCache );
$parserCacheFactory->method( 'getRevisionOutputCache' )->willReturn( $revisionCache );
$services = $this->getServiceContainer();
return new ParsoidOutputAccess(
new ServiceOptions(
ParsoidOutputAccess::CONSTRUCTOR_OPTIONS,
$services->getMainConfig(),
[ 'ParsoidWikiID' => 'MyWiki' ]
),
$parserCacheFactory,
$this->createNoOpMock( PageLookup::class ),
$services->getRevisionLookup(),
$services->getGlobalIdGenerator(),
new NullStatsdDataFactory(),
$parsoid,
$services->getParsoidSiteConfig(),
$services->getParsoidPageConfigFactory()
);
}
/**
* @dataProvider provideHandlesParsoidError
*/
@ -558,21 +613,9 @@ class HtmlOutputRendererHelperTest extends MediaWikiIntegrationTestCase {
->willThrowException( $parsoidException );
/** @var ParsoidOutputAccess|MockObject $access */
$access = new ParsoidOutputAccess(
new ServiceOptions(
ParsoidOutputAccess::CONSTRUCTOR_OPTIONS,
$this->getServiceContainer()->getMainConfig(),
[ 'ParsoidWikiID' => 'MyWiki' ]
),
$this->getServiceContainer()->getParserCacheFactory(),
$this->createNoOpMock( PageLookup::class ),
$this->getServiceContainer()->getRevisionLookup(),
$this->getServiceContainer()->getGlobalIdGenerator(),
new NullStatsdDataFactory(),
$parsoid,
$this->getServiceContainer()->getParsoidSiteConfig(),
$this->getServiceContainer()->getParsoidPageConfigFactory()
);
$access = $this->newRealParsoidOutputAccess( [
'parsoid' => $parsoid
] );
$helper = $this->newHelper( null, $access );
$helper->init( $page, self::PARAM_DEFAULTS, $this->newUser() );
@ -581,6 +624,51 @@ class HtmlOutputRendererHelperTest extends MediaWikiIntegrationTestCase {
$helper->getHtml();
}
public function testDisableParserCacheWrite() {
$page = $this->getExistingTestPage( __METHOD__ );
// NOTE: The save() method is not supported and will throw!
// The point of this test case is asserting that save() isn't called.
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'get' ] );
$parserCache->method( 'get' )->willReturn( false );
/** @var ParsoidOutputAccess|MockObject $access */
$access = $this->newRealParsoidOutputAccess( [
'parserCache' => $parserCache,
'revisionCache' => $this->createNoOpMock( RevisionOutputCache::class ),
] );
$helper = $this->newHelper( null, $access );
$helper->init( $page, self::PARAM_DEFAULTS, $this->newUser() );
// Set read = true, write = false
$helper->setUseParserCache( true, false );
$helper->getHtml();
}
public function testDisableParserCacheRead() {
$page = $this->getExistingTestPage( __METHOD__ );
// NOTE: The get() method is not supported and will throw!
// The point of this test case is asserting that get() isn't called.
// We also check that save() is still called.
$parserCache = $this->createNoOpMock( ParserCache::class, [ 'save' ] );
$parserCache->expects( $this->once() )->method( 'save' );
/** @var ParsoidOutputAccess|MockObject $access */
$access = $this->newRealParsoidOutputAccess( [
'parserCache' => $parserCache,
'revisionCache' => $this->createNoOpMock( RevisionOutputCache::class ),
] );
$helper = $this->newHelper( null, $access );
$helper->init( $page, self::PARAM_DEFAULTS, $this->newUser() );
// Set read = false, write = true
$helper->setUseParserCache( false, true );
$helper->getHtml();
}
/**
* Mock the language class based on a language code.
*

View file

@ -254,6 +254,33 @@ class ParsoidOutputAccessTest extends MediaWikiIntegrationTestCase {
$this->assertContainsHtml( self::MOCKED_HTML . ' of ' . self::WIKITEXT, $status );
}
/**
* Tests that getParserOutput() will force a parse since we know that
* the revision is not in the cache.
*
* @covers \MediaWiki\Parser\Parsoid\ParsoidOutputAccess::getParserOutput
*/
public function testLatestRevisionWithNoUpdateCache() {
$access = $this->getParsoidOutputAccessWithCache( 2 );
$parserOptions = $this->getParserOptions();
$page = $this->getNonexistingTestPage( __METHOD__ );
$this->editPage( $page, self::WIKITEXT );
$status = $access->getParserOutput(
$page,
$parserOptions,
null,
ParsoidOutputAccess::OPT_NO_UPDATE_CACHE
);
$this->assertContainsHtml( self::MOCKED_HTML . ' of ' . self::WIKITEXT, $status );
// Get the ParserOutput again, this should trigger a new parse
// since we suppressed caching above.
$status = $access->getParserOutput( $page, $parserOptions );
$this->assertContainsHtml( self::MOCKED_HTML . ' of ' . self::WIKITEXT, $status );
}
public function provideCacheThresholdData() {
return [
yield "fast parse" => [ 1, 2 ], // high threshold, no caching