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';
142 private LoggerSpi $loggerSpi;
154 LoggerSpi $loggerSpi,
160 $this->parserCacheFactory = $parserCacheFactory;
161 $this->revisionLookup = $revisionLookup;
162 $this->revisionRenderer = $revisionRenderer;
163 $this->statsFactory = $statsFactory;
164 $this->chronologyProtector = $chronologyProtector;
165 $this->loggerSpi = $loggerSpi;
166 $this->wikiPageFactory = $wikiPageFactory;
167 $this->titleFormatter = $titleFormatter;
168 $this->tracer = $tracer;
169 $this->poolCounterFactory = $poolCounterFactory;
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;
228 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
230 $useCache = $this->shouldUseCache( $page, $revision );
231 $primaryCache = $this->getPrimaryCache( $parserOptions );
232 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
234 if ( $useCache === self::CACHE_PRIMARY ) {
235 if ( $this->localCache->hasField( $classCacheKey, $page->
getLatest() ) && !$isOld ) {
236 return $this->localCache->getField( $classCacheKey, $page->
getLatest() );
238 $output = $primaryCache->get( $page, $parserOptions );
239 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
240 $secondaryCache = $this->getSecondaryCache( $parserOptions );
241 $output = $secondaryCache->get( $revision, $parserOptions );
246 $statType = $statReason = $output ?
'hit' :
'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()
262 $statReason =
'obsolete';
267 if ( $output && !$isOld ) {
268 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
272 ->getCounter(
'parseroutputaccess_cache_total' )
273 ->setLabel(
'cache', $useCache )
274 ->setLabel(
'reason', $statReason )
275 ->setLabel(
'type', $statType )
276 ->copyToStatsdAt(
"ParserOutputAccess.Cache.$useCache.$statReason" )
279 return $output ?:
null;
310 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
311 $error = $this->checkPreconditions( $page, $revision, $options );
314 ->getCounter(
'parseroutputaccess_case' )
315 ->setLabel(
'case',
'error' )
316 ->copyToStatsdAt(
'ParserOutputAccess.Case.error' )
324 ->getCounter(
'parseroutputaccess_case' )
325 ->setLabel(
'case',
'old' )
326 ->copyToStatsdAt(
'ParserOutputAccess.Case.old' )
330 ->getCounter(
'parseroutputaccess_case' )
331 ->setLabel(
'case',
'current' )
332 ->copyToStatsdAt(
'ParserOutputAccess.Case.current' )
336 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
337 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
339 return Status::newGood( $output );
345 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) :
null;
349 ->getCounter(
'parseroutputaccess_status' )
350 ->setLabel(
'status',
'norev' )
351 ->copyToStatsdAt(
"ParserOutputAccess.Status.norev" )
353 return Status::newFatal(
'missing-revision', $revId );
357 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
358 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
360 $status = $work->execute();
364 $status = $this->renderRevision( $page, $parserOptions, $revision, $options,
null );
367 $output = $status->getValue();
368 Assert::postcondition( $output || !$status->isOK(),
'Inconsistent status' );
370 if ( $output && !$isOld ) {
371 $primaryCache = $this->getPrimaryCache( $parserOptions );
372 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
373 $this->localCache->setField( $classCacheKey, $page->
getLatest(), $output );
376 if ( $status->isGood() ) {
377 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
378 ->setLabel(
'status',
'good' )
379 ->copyToStatsdAt(
'ParserOutputAccess.Status.good' )
381 } elseif ( $status->isOK() ) {
382 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
383 ->setLabel(
'status',
'ok' )
384 ->copyToStatsdAt(
'ParserOutputAccess.Status.ok' )
387 $this->statsFactory->getCounter(
'parseroutputaccess_status' )
388 ->setLabel(
'status',
'error' )
389 ->copyToStatsdAt(
'ParserOutputAccess.Status.error' )
410 private function renderRevision(
412 ParserOptions $parserOptions,
413 RevisionRecord $revision,
415 ?ParserOutput $previousOutput =
null
417 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
418 $this->statsFactory->getCounter(
'parseroutputaccess_render_total' )
419 ->setLabel(
'pool',
'none' )
420 ->setLabel(
'cache', self::CACHE_NONE )
421 ->copyToStatsdAt(
'ParserOutputAccess.PoolWork.None' )
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 $statCacheLabelLegacy = [
556 self::CACHE_PRIMARY =>
'Current',
557 self::CACHE_SECONDARY =>
'Old',
558 ][$useCache] ??
'Uncached';
560 $this->statsFactory->getCounter(
'parseroutputaccess_render_total' )
561 ->setLabel(
'pool',
'articleview' )
562 ->setLabel(
'cache', $useCache )
563 ->copyToStatsdAt(
"ParserOutputAccess.PoolWork.$statCacheLabelLegacy" )
566 switch ( $useCache ) {
567 case self::CACHE_PRIMARY:
568 $primaryCache = $this->getPrimaryCache( $parserOptions );
569 $parserCacheMetadata = $primaryCache->getMetadata( $page );
570 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
571 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
574 $workKey = $cacheKey .
':revid:' . $revision->
getId();
576 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
582 $this->revisionRenderer,
584 $this->chronologyProtector,
586 $this->wikiPageFactory,
587 !( $options & self::OPT_NO_UPDATE_CACHE ),
588 (
bool)( $options & self::OPT_LINKS_UPDATE )
591 case self::CACHE_SECONDARY:
592 $secondaryCache = $this->getSecondaryCache( $parserOptions );
593 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
594 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
600 $this->revisionRenderer,
607 $secondaryCache = $this->getSecondaryCache( $parserOptions );
608 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
609 $pool = $this->poolCounterFactory->create(
'ArticleView', $workKey );
614 $this->revisionRenderer,
622 private function getPrimaryCache( ParserOptions $pOpts ):
ParserCache {
623 if ( $pOpts->getUseParsoid() ) {
624 return $this->parserCacheFactory->getParserCache(
625 self::PARSOID_PCACHE_NAME
629 return $this->parserCacheFactory->getParserCache(
630 ParserCacheFactory::DEFAULT_NAME
634 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
635 if ( $pOpts->getUseParsoid() ) {
636 return $this->parserCacheFactory->getRevisionOutputCache(
637 self::PARSOID_RCACHE_NAME
641 return $this->parserCacheFactory->getRevisionOutputCache(
642 ParserCacheFactory::DEFAULT_RCACHE_NAME
646 private function startOperationSpan(
649 ?RevisionRecord $revision =
null
651 $span = $this->tracer->createSpan(
"ParserOutputAccess::$opName" );
652 if ( $span->getContext()->isSampled() ) {
653 $span->setAttributes( [
654 'org.wikimedia.parser.page' => $page->__toString(),
655 'org.wikimedia.parser.page.id' => $page->getId(),
656 'org.wikimedia.parser.page.wiki' => $page->getWikiId(),
659 $span->setAttributes( [
660 'org.wikimedia.parser.revision.id' => $revision->
getId(),
661 'org.wikimedia.parser.revision.parent_id' => $revision->
getParentId(),
665 return $span->start()->activate();