70 public const OPT_NO_CHECK_CACHE = 1;
73 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
78 public const OPT_NO_UPDATE_CACHE = 2;
85 public const OPT_NO_AUDIENCE_CHECK = 4;
91 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
97 public const OPT_LINKS_UPDATE = 8;
117 public const OPT_IGNORE_PROFILE_VERSION = 128;
123 private const CACHE_PRIMARY =
'primary';
126 private const CACHE_SECONDARY =
'secondary';
141 private LoggerSpi $loggerSpi;
152 LoggerSpi $loggerSpi,
156 $this->parserCacheFactory = $parserCacheFactory;
157 $this->revisionLookup = $revisionLookup;
158 $this->revisionRenderer = $revisionRenderer;
159 $this->statsFactory = $statsFactory;
160 $this->lbFactory = $lbFactory;
161 $this->chronologyProtector = $chronologyProtector;
162 $this->loggerSpi = $loggerSpi;
163 $this->wikiPageFactory = $wikiPageFactory;
164 $this->titleFormatter = $titleFormatter;
177 private function shouldUseCache(
181 if ( $rev && !$rev->
getId() ) {
190 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
191 if ( !$page->
exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
197 return self::CACHE_PRIMARY;
200 if ( !$rev->
audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
205 return self::CACHE_SECONDARY;
224 $isOld = $revision && $revision->getId() !== $page->getLatest();
225 $useCache = $this->shouldUseCache( $page, $revision );
226 $primaryCache = $this->getPrimaryCache( $parserOptions );
227 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
229 if ( $useCache === self::CACHE_PRIMARY ) {
230 if ( $this->localCache->hasField( $classCacheKey, $page->
getLatest() ) && !$isOld ) {
231 return $this->localCache->getField( $classCacheKey, $page->
getLatest() );
233 $output = $primaryCache->get( $page, $parserOptions );
234 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
235 $secondaryCache = $this->getSecondaryCache( $parserOptions );
236 $output = $secondaryCache->get( $revision, $parserOptions );
241 $notHitReason =
'miss';
243 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
246 $pageBundleData = $output->getExtensionData(
247 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
250 $cachedVersion = $pageBundleData[
'version'] ??
null;
252 $cachedVersion !==
null &&
253 $cachedVersion !== Parsoid::defaultHTMLVersion()
255 $notHitReason =
'obsolete';
260 if ( $output && !$isOld ) {
261 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
266 ->getCounter(
'parseroutputaccess_cache' )
267 ->setLabel(
'cache', $useCache )
268 ->setLabel(
'reason',
'hit' )
269 ->setLabel(
'type',
'hit' )
270 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.hit" )
274 ->getCounter(
'parseroutputaccess_cache' )
275 ->setLabel(
'reason', $notHitReason )
276 ->setLabel(
'cache', $useCache )
277 ->setLabel(
'type',
'miss' )
278 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.$notHitReason" )
282 return $output ?:
null;
313 $error = $this->checkPreconditions( $page, $revision, $options );
316 ->getCounter(
'parseroutputaccess_case' )
317 ->setLabel(
'case',
'error' )
318 ->copyToStatsdAt(
'ParserOutputAccess.Case.error' )
326 ->getCounter(
'parseroutputaccess_case' )
327 ->setLabel(
'case',
'old' )
328 ->copyToStatsdAt(
'ParserOutputAccess.Case.old' )
332 ->getCounter(
'parseroutputaccess_case' )
333 ->setLabel(
'case',
'current' )
334 ->copyToStatsdAt(
'ParserOutputAccess.Case.current' )
338 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
339 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
341 return Status::newGood( $output );
347 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) :
null;
351 ->getCounter(
'parseroutputaccess_status' )
352 ->setLabel(
'status',
'norev' )
353 ->copyToStatsdAt(
"ParserOutputAccess.Status.norev" )
355 return Status::newFatal(
'missing-revision', $revId );
359 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
360 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
362 $status = $work->execute();
366 $status = $this->renderRevision( $page, $parserOptions, $revision, $options,
null );
369 $output = $status->getValue();
370 Assert::postcondition( $output || !$status->isOK(),
'Inconsistent status' );
372 if ( $output && !$isOld ) {
373 $primaryCache = $this->getPrimaryCache( $parserOptions );
374 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
375 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
378 if ( $status->isGood() ) {
379 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
380 ->setLabel(
'status',
'good' )
381 ->copyToStatsdAt(
'ParserOutputAccess.Status.good' )
383 } elseif ( $status->isOK() ) {
384 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
385 ->setLabel(
'status',
'ok' )
386 ->copyToStatsdAt(
'ParserOutputAccess.Status.ok' )
389 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
390 ->setLabel(
'status',
'error' )
391 ->copyToStatsdAt(
'ParserOutputAccess.Status.error' )
412 private function renderRevision(
414 ParserOptions $parserOptions,
415 RevisionRecord $revision,
417 ?ParserOutput $previousOutput =
null
419 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
424 $useCache = $this->shouldUseCache( $page, $revision );
428 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
429 MainConfigNames::ParsoidSelectiveUpdateSampleRate
431 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
433 if ( $previousOutput ===
null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
439 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
440 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?:
null;
444 $renderedRev = $this->revisionRenderer->getRenderedRevision(
449 'audience' => RevisionRecord::RAW,
450 'previous-output' => $previousOutput,
454 $output = $renderedRev->getRevisionParserOutput();
457 $content = $revision->getContent( SlotRecord::MAIN );
459 'source' =>
'ParserOutputAccess',
460 'type' => $previousOutput ===
null ?
'full' :
'selective',
461 'reason' => $parserOptions->getRenderReason(),
462 'parser' => $parserOptions->getUseParsoid() ?
'parsoid' :
'legacy',
463 'opportunistic' =>
'false',
464 'wiki' => WikiMap::getCurrentWikiId(),
465 'model' => $content ? $content->getModel() :
'unknown',
468 ->getCounter(
'ParserCache_selective_total' )
469 ->setLabels( $labels )
472 ->getCounter(
'ParserCache_selective_cpu_seconds' )
473 ->setLabels( $labels )
474 ->incrementBy( $output->getTimeProfile(
'cpu' ) );
477 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
478 if ( $useCache === self::CACHE_PRIMARY ) {
479 $primaryCache = $this->getPrimaryCache( $parserOptions );
480 $primaryCache->save( $output, $page, $parserOptions );
481 } elseif ( $useCache === self::CACHE_SECONDARY ) {
482 $secondaryCache = $this->getSecondaryCache( $parserOptions );
483 $secondaryCache->save( $output, $revision, $parserOptions );
487 if ( $options & self::OPT_LINKS_UPDATE ) {
488 $this->wikiPageFactory->newFromTitle( $page )
489 ->triggerOpportunisticLinksUpdate( $output );
492 return Status::newGood( $output );
502 private function checkPreconditions(
504 ?RevisionRecord $revision =
null,
507 if ( !$page->exists() ) {
508 return Status::newFatal(
'nopagetext' );
511 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
512 throw new InvalidArgumentException(
513 'The revision does not have a known ID. Use OPT_NO_CACHE.'
517 if ( $revision && $revision->getPageId() !== $page->getId() ) {
518 throw new InvalidArgumentException(
519 'The revision does not belong to the given page.'
523 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
526 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
527 return Status::newFatal(
528 'missing-revision-permission',
530 $revision->getTimestamp(),
531 $this->titleFormatter->getPrefixedDBkey( $page )
553 $useCache = $this->shouldUseCache( $page, $revision );
555 switch ( $useCache ) {
556 case self::CACHE_PRIMARY:
557 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
558 ->setLabel(
'cache', self::CACHE_PRIMARY )
559 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Current' )
561 $primaryCache = $this->getPrimaryCache( $parserOptions );
562 $parserCacheMetadata = $primaryCache->getMetadata( $page );
563 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
564 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
567 $workKey = $cacheKey .
':revid:' . $revision->
getId();
574 $this->revisionRenderer,
577 $this->chronologyProtector,
579 $this->wikiPageFactory,
580 !( $options & self::OPT_NO_UPDATE_CACHE ),
581 (
bool)( $options & self::OPT_LINKS_UPDATE )
584 case self::CACHE_SECONDARY:
585 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
586 ->setLabel(
'cache', self::CACHE_SECONDARY )
587 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Old' )
589 $secondaryCache = $this->getSecondaryCache( $parserOptions );
590 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
596 $this->revisionRenderer,
601 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
602 ->setLabel(
'cache', self::CACHE_NONE )
603 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Uncached' )
605 $secondaryCache = $this->getSecondaryCache( $parserOptions );
606 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
611 $this->revisionRenderer,
619 private function getPrimaryCache( ParserOptions $pOpts ):
ParserCache {
620 if ( $pOpts->getUseParsoid() ) {
621 return $this->parserCacheFactory->getParserCache(
622 self::PARSOID_PCACHE_NAME
626 return $this->parserCacheFactory->getParserCache(
627 ParserCacheFactory::DEFAULT_NAME
631 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
632 if ( $pOpts->getUseParsoid() ) {
633 return $this->parserCacheFactory->getRevisionOutputCache(
634 self::PARSOID_RCACHE_NAME
638 return $this->parserCacheFactory->getRevisionOutputCache(
639 ParserCacheFactory::DEFAULT_RCACHE_NAME