73 public const OPT_NO_CHECK_CACHE = 1;
76 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
81 public const OPT_NO_UPDATE_CACHE = 2;
88 public const OPT_NO_AUDIENCE_CHECK = 4;
94 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
100 public const OPT_LINKS_UPDATE = 8;
120 public const OPT_IGNORE_PROFILE_VERSION = 128;
126 private const CACHE_PRIMARY =
'primary';
129 private const CACHE_SECONDARY =
'secondary';
144 private LoggerSpi $loggerSpi;
157 LoggerSpi $loggerSpi,
163 $this->parserCacheFactory = $parserCacheFactory;
164 $this->revisionLookup = $revisionLookup;
165 $this->revisionRenderer = $revisionRenderer;
166 $this->statsFactory = $statsFactory;
167 $this->lbFactory = $lbFactory;
168 $this->chronologyProtector = $chronologyProtector;
169 $this->loggerSpi = $loggerSpi;
170 $this->wikiPageFactory = $wikiPageFactory;
171 $this->titleFormatter = $titleFormatter;
172 $this->tracer = $tracer;
173 $this->poolCounterFactory = $poolCounterFactory;
186 private function shouldUseCache(
190 if ( $rev && !$rev->
getId() ) {
199 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
200 if ( !$page->
exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
206 return self::CACHE_PRIMARY;
209 if ( !$rev->
audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
214 return self::CACHE_SECONDARY;
232 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
234 $useCache = $this->shouldUseCache( $page, $revision );
235 $primaryCache = $this->getPrimaryCache( $parserOptions );
236 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
238 if ( $useCache === self::CACHE_PRIMARY ) {
239 if ( $this->localCache->hasField( $classCacheKey, $page->
getLatest() ) && !$isOld ) {
240 return $this->localCache->getField( $classCacheKey, $page->
getLatest() );
242 $output = $primaryCache->get( $page, $parserOptions );
243 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
244 $secondaryCache = $this->getSecondaryCache( $parserOptions );
245 $output = $secondaryCache->get( $revision, $parserOptions );
250 $statType = $statReason = $output ?
'hit' :
'miss';
253 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
256 $pageBundleData = $output->getExtensionData(
257 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
260 $cachedVersion = $pageBundleData[
'version'] ??
null;
262 $cachedVersion !==
null &&
263 $cachedVersion !== Parsoid::defaultHTMLVersion()
266 $statReason =
'obsolete';
271 if ( $output && !$isOld ) {
272 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
276 ->getCounter(
'parseroutputaccess_cache_total' )
277 ->setLabel(
'cache', $useCache )
278 ->setLabel(
'reason', $statReason )
279 ->setLabel(
'type', $statType )
280 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.$statReason" )
283 return $output ?:
null;
314 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
315 $error = $this->checkPreconditions( $page, $revision, $options );
318 ->getCounter(
'parseroutputaccess_case' )
319 ->setLabel(
'case',
'error' )
320 ->copyToStatsdAt(
'ParserOutputAccess.Case.error' )
328 ->getCounter(
'parseroutputaccess_case' )
329 ->setLabel(
'case',
'old' )
330 ->copyToStatsdAt(
'ParserOutputAccess.Case.old' )
334 ->getCounter(
'parseroutputaccess_case' )
335 ->setLabel(
'case',
'current' )
336 ->copyToStatsdAt(
'ParserOutputAccess.Case.current' )
340 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
341 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
343 return Status::newGood( $output );
349 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) :
null;
353 ->getCounter(
'parseroutputaccess_status' )
354 ->setLabel(
'status',
'norev' )
355 ->copyToStatsdAt(
"ParserOutputAccess.Status.norev" )
357 return Status::newFatal(
'missing-revision', $revId );
361 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
362 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
364 $status = $work->execute();
368 $status = $this->renderRevision( $page, $parserOptions, $revision, $options,
null );
371 $output = $status->getValue();
372 Assert::postcondition( $output || !$status->isOK(),
'Inconsistent status' );
374 if ( $output && !$isOld ) {
375 $primaryCache = $this->getPrimaryCache( $parserOptions );
376 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
377 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
380 if ( $status->isGood() ) {
381 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
382 ->setLabel(
'status',
'good' )
383 ->copyToStatsdAt(
'ParserOutputAccess.Status.good' )
385 } elseif ( $status->isOK() ) {
386 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
387 ->setLabel(
'status',
'ok' )
388 ->copyToStatsdAt(
'ParserOutputAccess.Status.ok' )
391 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
392 ->setLabel(
'status',
'error' )
393 ->copyToStatsdAt(
'ParserOutputAccess.Status.error' )
414 private function renderRevision(
416 ParserOptions $parserOptions,
417 RevisionRecord $revision,
419 ?ParserOutput $previousOutput =
null
421 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
422 $this->statsFactory->getCounter(
'parseroutputaccess_render_total' )
423 ->setLabel(
'pool',
'none' )
424 ->setLabel(
'cache', self::CACHE_NONE )
425 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.None' )
428 $useCache = $this->shouldUseCache( $page, $revision );
432 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
433 MainConfigNames::ParsoidSelectiveUpdateSampleRate
435 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
437 if ( $previousOutput ===
null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
443 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
444 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?:
null;
448 $renderedRev = $this->revisionRenderer->getRenderedRevision(
453 'audience' => RevisionRecord::RAW,
454 'previous-output' => $previousOutput,
458 $output = $renderedRev->getRevisionParserOutput();
461 $content = $revision->getContent( SlotRecord::MAIN );
463 'source' =>
'ParserOutputAccess',
464 'type' => $previousOutput ===
null ?
'full' :
'selective',
465 'reason' => $parserOptions->getRenderReason(),
466 'parser' => $parserOptions->getUseParsoid() ?
'parsoid' :
'legacy',
467 'opportunistic' =>
'false',
468 'wiki' => WikiMap::getCurrentWikiId(),
469 'model' => $content ? $content->getModel() :
'unknown',
472 ->getCounter(
'ParserCache_selective_total' )
473 ->setLabels( $labels )
476 ->getCounter(
'ParserCache_selective_cpu_seconds' )
477 ->setLabels( $labels )
478 ->incrementBy( $output->getTimeProfile(
'cpu' ) );
481 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
482 if ( $useCache === self::CACHE_PRIMARY ) {
483 $primaryCache = $this->getPrimaryCache( $parserOptions );
484 $primaryCache->save( $output, $page, $parserOptions );
485 } elseif ( $useCache === self::CACHE_SECONDARY ) {
486 $secondaryCache = $this->getSecondaryCache( $parserOptions );
487 $secondaryCache->save( $output, $revision, $parserOptions );
491 if ( $options & self::OPT_LINKS_UPDATE ) {
492 $this->wikiPageFactory->newFromTitle( $page )
493 ->triggerOpportunisticLinksUpdate( $output );
496 return Status::newGood( $output );
506 private function checkPreconditions(
508 ?RevisionRecord $revision =
null,
511 if ( !$page->exists() ) {
512 return Status::newFatal(
'nopagetext' );
515 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
516 throw new InvalidArgumentException(
517 'The revision does not have a known ID. Use OPT_NO_CACHE.'
521 if ( $revision && $revision->getPageId() !== $page->getId() ) {
522 throw new InvalidArgumentException(
523 'The revision does not belong to the given page.'
527 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
530 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
531 return Status::newFatal(
532 'missing-revision-permission',
534 $revision->getTimestamp(),
535 $this->titleFormatter->getPrefixedDBkey( $page )
557 $useCache = $this->shouldUseCache( $page, $revision );
559 $statCacheLabelLegacy = [
560 self::CACHE_PRIMARY =>
'Current',
561 self::CACHE_SECONDARY =>
'Old',
562 ][$useCache] ??
'Uncached';
564 $this->statsFactory->getCounter(
'parseroutputaccess_render_total' )
565 ->setLabel(
'pool',
'articleview' )
566 ->setLabel(
'cache', $useCache )
567 ->copyToStatsdAt(
"ParserOutputAccess.PoolWork.$statCacheLabelLegacy" )
570 switch ( $useCache ) {
571 case self::CACHE_PRIMARY:
572 $primaryCache = $this->getPrimaryCache( $parserOptions );
573 $parserCacheMetadata = $primaryCache->getMetadata( $page );
574 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
575 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
578 $workKey = $cacheKey .
':revid:' . $revision->
getId();
580 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
586 $this->revisionRenderer,
589 $this->chronologyProtector,
591 $this->wikiPageFactory,
592 !( $options & self::OPT_NO_UPDATE_CACHE ),
593 (
bool)( $options & self::OPT_LINKS_UPDATE )
596 case self::CACHE_SECONDARY:
597 $secondaryCache = $this->getSecondaryCache( $parserOptions );
598 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
599 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
605 $this->revisionRenderer,
612 $secondaryCache = $this->getSecondaryCache( $parserOptions );
613 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
614 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
619 $this->revisionRenderer,
627 private function getPrimaryCache( ParserOptions $pOpts ):
ParserCache {
628 if ( $pOpts->getUseParsoid() ) {
629 return $this->parserCacheFactory->getParserCache(
630 self::PARSOID_PCACHE_NAME
634 return $this->parserCacheFactory->getParserCache(
635 ParserCacheFactory::DEFAULT_NAME
639 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
640 if ( $pOpts->getUseParsoid() ) {
641 return $this->parserCacheFactory->getRevisionOutputCache(
642 self::PARSOID_RCACHE_NAME
646 return $this->parserCacheFactory->getRevisionOutputCache(
647 ParserCacheFactory::DEFAULT_RCACHE_NAME
651 private function startOperationSpan(
654 ?RevisionRecord $revision =
null
656 $span = $this->tracer->createSpan(
"ParserOutputAccess::$opName" );
657 if ( $span->getContext()->isSampled() ) {
658 $span->setAttributes( [
659 'org.wikimedia.parser.page' => $page->__toString(),
660 'org.wikimedia.parser.page.id' => $page->getId(),
661 'org.wikimedia.parser.page.wiki' => $page->getWikiId(),
664 $span->setAttributes( [
665 'org.wikimedia.parser.revision.id' => $revision->
getId(),
666 'org.wikimedia.parser.revision.parent_id' => $revision->
getParentId(),
670 return $span->start()->activate();