72 public const OPT_NO_CHECK_CACHE = 1;
75 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
80 public const OPT_NO_UPDATE_CACHE = 2;
87 public const OPT_NO_AUDIENCE_CHECK = 4;
93 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
99 public const OPT_LINKS_UPDATE = 8;
119 public const OPT_IGNORE_PROFILE_VERSION = 128;
125 private const CACHE_PRIMARY =
'primary';
128 private const CACHE_SECONDARY =
'secondary';
143 private LoggerSpi $loggerSpi;
155 LoggerSpi $loggerSpi,
160 $this->parserCacheFactory = $parserCacheFactory;
161 $this->revisionLookup = $revisionLookup;
162 $this->revisionRenderer = $revisionRenderer;
163 $this->statsFactory = $statsFactory;
164 $this->lbFactory = $lbFactory;
165 $this->chronologyProtector = $chronologyProtector;
166 $this->loggerSpi = $loggerSpi;
167 $this->wikiPageFactory = $wikiPageFactory;
168 $this->titleFormatter = $titleFormatter;
169 $this->tracer = $tracer;
182 private function shouldUseCache(
186 if ( $rev && !$rev->
getId() ) {
195 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
196 if ( !$page->
exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
202 return self::CACHE_PRIMARY;
205 if ( !$rev->
audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
210 return self::CACHE_SECONDARY;
229 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
231 $useCache = $this->shouldUseCache( $page, $revision );
232 $primaryCache = $this->getPrimaryCache( $parserOptions );
233 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
235 if ( $useCache === self::CACHE_PRIMARY ) {
236 if ( $this->localCache->hasField( $classCacheKey, $page->
getLatest() ) && !$isOld ) {
237 return $this->localCache->getField( $classCacheKey, $page->
getLatest() );
239 $output = $primaryCache->get( $page, $parserOptions );
240 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
241 $secondaryCache = $this->getSecondaryCache( $parserOptions );
242 $output = $secondaryCache->get( $revision, $parserOptions );
247 $notHitReason =
'miss';
249 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
252 $pageBundleData = $output->getExtensionData(
253 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
256 $cachedVersion = $pageBundleData[
'version'] ??
null;
258 $cachedVersion !==
null &&
259 $cachedVersion !== Parsoid::defaultHTMLVersion()
261 $notHitReason =
'obsolete';
266 if ( $output && !$isOld ) {
267 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
272 ->getCounter(
'parseroutputaccess_cache' )
273 ->setLabel(
'cache', $useCache )
274 ->setLabel(
'reason',
'hit' )
275 ->setLabel(
'type',
'hit' )
276 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.hit" )
280 ->getCounter(
'parseroutputaccess_cache' )
281 ->setLabel(
'reason', $notHitReason )
282 ->setLabel(
'cache', $useCache )
283 ->setLabel(
'type',
'miss' )
284 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.$notHitReason" )
288 return $output ?:
null;
319 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
320 $error = $this->checkPreconditions( $page, $revision, $options );
323 ->getCounter(
'parseroutputaccess_case' )
324 ->setLabel(
'case',
'error' )
325 ->copyToStatsdAt(
'ParserOutputAccess.Case.error' )
333 ->getCounter(
'parseroutputaccess_case' )
334 ->setLabel(
'case',
'old' )
335 ->copyToStatsdAt(
'ParserOutputAccess.Case.old' )
339 ->getCounter(
'parseroutputaccess_case' )
340 ->setLabel(
'case',
'current' )
341 ->copyToStatsdAt(
'ParserOutputAccess.Case.current' )
345 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
346 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
348 return Status::newGood( $output );
354 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) :
null;
358 ->getCounter(
'parseroutputaccess_status' )
359 ->setLabel(
'status',
'norev' )
360 ->copyToStatsdAt(
"ParserOutputAccess.Status.norev" )
362 return Status::newFatal(
'missing-revision', $revId );
366 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
367 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
369 $status = $work->execute();
373 $status = $this->renderRevision( $page, $parserOptions, $revision, $options,
null );
376 $output = $status->getValue();
377 Assert::postcondition( $output || !$status->isOK(),
'Inconsistent status' );
379 if ( $output && !$isOld ) {
380 $primaryCache = $this->getPrimaryCache( $parserOptions );
381 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
382 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
385 if ( $status->isGood() ) {
386 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
387 ->setLabel(
'status',
'good' )
388 ->copyToStatsdAt(
'ParserOutputAccess.Status.good' )
390 } elseif ( $status->isOK() ) {
391 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
392 ->setLabel(
'status',
'ok' )
393 ->copyToStatsdAt(
'ParserOutputAccess.Status.ok' )
396 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
397 ->setLabel(
'status',
'error' )
398 ->copyToStatsdAt(
'ParserOutputAccess.Status.error' )
419 private function renderRevision(
421 ParserOptions $parserOptions,
422 RevisionRecord $revision,
424 ?ParserOutput $previousOutput =
null
426 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
427 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
428 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.None' )
429 ->setLabel(
'cache', self::CACHE_NONE )
432 $useCache = $this->shouldUseCache( $page, $revision );
436 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
437 MainConfigNames::ParsoidSelectiveUpdateSampleRate
439 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
441 if ( $previousOutput ===
null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
447 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
448 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?:
null;
452 $renderedRev = $this->revisionRenderer->getRenderedRevision(
457 'audience' => RevisionRecord::RAW,
458 'previous-output' => $previousOutput,
462 $output = $renderedRev->getRevisionParserOutput();
465 $content = $revision->getContent( SlotRecord::MAIN );
467 'source' =>
'ParserOutputAccess',
468 'type' => $previousOutput ===
null ?
'full' :
'selective',
469 'reason' => $parserOptions->getRenderReason(),
470 'parser' => $parserOptions->getUseParsoid() ?
'parsoid' :
'legacy',
471 'opportunistic' =>
'false',
472 'wiki' => WikiMap::getCurrentWikiId(),
473 'model' => $content ? $content->getModel() :
'unknown',
476 ->getCounter(
'ParserCache_selective_total' )
477 ->setLabels( $labels )
480 ->getCounter(
'ParserCache_selective_cpu_seconds' )
481 ->setLabels( $labels )
482 ->incrementBy( $output->getTimeProfile(
'cpu' ) );
485 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $output->isCacheable() ) {
486 if ( $useCache === self::CACHE_PRIMARY ) {
487 $primaryCache = $this->getPrimaryCache( $parserOptions );
488 $primaryCache->save( $output, $page, $parserOptions );
489 } elseif ( $useCache === self::CACHE_SECONDARY ) {
490 $secondaryCache = $this->getSecondaryCache( $parserOptions );
491 $secondaryCache->save( $output, $revision, $parserOptions );
495 if ( $options & self::OPT_LINKS_UPDATE ) {
496 $this->wikiPageFactory->newFromTitle( $page )
497 ->triggerOpportunisticLinksUpdate( $output );
500 return Status::newGood( $output );
510 private function checkPreconditions(
512 ?RevisionRecord $revision =
null,
515 if ( !$page->exists() ) {
516 return Status::newFatal(
'nopagetext' );
519 if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) {
520 throw new InvalidArgumentException(
521 'The revision does not have a known ID. Use OPT_NO_CACHE.'
525 if ( $revision && $revision->getPageId() !== $page->getId() ) {
526 throw new InvalidArgumentException(
527 'The revision does not belong to the given page.'
531 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
534 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
535 return Status::newFatal(
536 'missing-revision-permission',
538 $revision->getTimestamp(),
539 $this->titleFormatter->getPrefixedDBkey( $page )
561 $useCache = $this->shouldUseCache( $page, $revision );
563 switch ( $useCache ) {
564 case self::CACHE_PRIMARY:
565 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
566 ->setLabel(
'cache', self::CACHE_PRIMARY )
567 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Current' )
569 $primaryCache = $this->getPrimaryCache( $parserOptions );
570 $parserCacheMetadata = $primaryCache->getMetadata( $page );
571 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
572 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
575 $workKey = $cacheKey .
':revid:' . $revision->
getId();
582 $this->revisionRenderer,
585 $this->chronologyProtector,
587 $this->wikiPageFactory,
588 !( $options & self::OPT_NO_UPDATE_CACHE ),
589 (
bool)( $options & self::OPT_LINKS_UPDATE )
592 case self::CACHE_SECONDARY:
593 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
594 ->setLabel(
'cache', self::CACHE_SECONDARY )
595 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Old' )
597 $secondaryCache = $this->getSecondaryCache( $parserOptions );
598 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
604 $this->revisionRenderer,
609 $this->statsFactory->getCounter(
'parseroutputaccess_poolwork' )
610 ->setLabel(
'cache', self::CACHE_NONE )
611 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.Uncached' )
613 $secondaryCache = $this->getSecondaryCache( $parserOptions );
614 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
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();