MediaWiki master
ParserOutputAccess.php
Go to the documentation of this file.
1<?php
6namespace MediaWiki\Page;
7
8use InvalidArgumentException;
27use Psr\Log\LoggerAwareInterface;
28use Psr\Log\LoggerInterface;
29use Psr\Log\NullLogger;
30use Wikimedia\Assert\Assert;
32use Wikimedia\Parsoid\Parsoid;
37
47class ParserOutputAccess implements LoggerAwareInterface {
48
50 public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME;
51
53 public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME;
54
56 public const POSTPROC_CACHE_PREFIX = 'postproc-';
57
61 public const OPT_NO_CHECK_CACHE = 1;
62
64 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
65
69 public const OPT_NO_UPDATE_CACHE = 2;
70
76 public const OPT_NO_AUDIENCE_CHECK = 4;
77
82 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
83
88 public const OPT_LINKS_UPDATE = 8;
89
104 public const OPT_FOR_ARTICLE_VIEW = 16;
105
110 public const OPT_IGNORE_PROFILE_VERSION = 128;
111
115 public const OPT_NO_POSTPROC_CACHE = 256;
116
121 public const OPT_POOL_COUNTER_FALLBACK = 'poolcounter-fallback';
122
126 public const OPT_POOL_COUNTER = 'poolcounter-type';
127
131 public const POOL_COUNTER_ARTICLE_VIEW = 'ArticleView';
132
136 public const POOL_COUNTER_REST_API = 'HtmlRestApi';
137
142 private const DEFAULT_OPTIONS = [
143 self::OPT_POOL_COUNTER => null,
144 self::OPT_POOL_COUNTER_FALLBACK => false
145 ];
146
148 private const CACHE_NONE = 'none';
149
151 private const CACHE_PRIMARY = 'primary';
152
154 private const CACHE_SECONDARY = 'secondary';
155
161 private MapCacheLRU $localCache;
162
163 private ParserCacheFactory $parserCacheFactory;
164 private RevisionLookup $revisionLookup;
165 private RevisionRenderer $revisionRenderer;
166 private StatsFactory $statsFactory;
167 private ChronologyProtector $chronologyProtector;
168 private WikiPageFactory $wikiPageFactory;
169 private TitleFormatter $titleFormatter;
170 private TracerInterface $tracer;
171 private PoolCounterFactory $poolCounterFactory;
172 private LoggerInterface $logger;
173
174 public function __construct(
175 private readonly Config $config,
176 private readonly OutputTransformPipeline $outputTransformPipeline,
177 ParserCacheFactory $parserCacheFactory,
178 RevisionLookup $revisionLookup,
179 RevisionRenderer $revisionRenderer,
180 StatsFactory $statsFactory,
181 ChronologyProtector $chronologyProtector,
182 WikiPageFactory $wikiPageFactory,
183 TitleFormatter $titleFormatter,
184 TracerInterface $tracer,
185 PoolCounterFactory $poolCounterFactory
186 ) {
187 $this->parserCacheFactory = $parserCacheFactory;
188 $this->revisionLookup = $revisionLookup;
189 $this->revisionRenderer = $revisionRenderer;
190 $this->statsFactory = $statsFactory;
191 $this->chronologyProtector = $chronologyProtector;
192 $this->wikiPageFactory = $wikiPageFactory;
193 $this->titleFormatter = $titleFormatter;
194 $this->tracer = $tracer;
195 $this->poolCounterFactory = $poolCounterFactory;
196
197 $this->localCache = new MapCacheLRU( 10 );
198 $this->logger = new NullLogger();
199 }
200
201 public function setLogger( LoggerInterface $logger ): void {
202 $this->logger = $logger;
203 }
204
216 private static function normalizeOptions( $options ): array {
217 $bits = 0;
218
219 // TODO: Starting in 1.46, emit deprecation warnings when getting an int.
220
221 if ( is_array( $options ) ) {
222 if ( $options['_normalized_'] ?? false ) {
223 // already normalized.
224 return $options;
225 }
226
227 // Collect all bits from array keys, in case one of the keys
228 // sets multiple bits.
229 foreach ( $options as $opt => $enabled ) {
230 if ( is_int( $opt ) && $enabled === true ) {
231 $bits |= $opt;
232 }
233 }
234 } else {
235 $bits = $options;
236 $options = [];
237 }
238
239 // From the (numerically) smallest to the largest option that can possibly exist
240 for ( $b = self::OPT_NO_CHECK_CACHE; $b <= self::OPT_NO_POSTPROC_CACHE; $b <<= 1 ) {
241 $options[$b] = (bool)( $bits & $b );
242 }
243
244 if ( $options[ self::OPT_FOR_ARTICLE_VIEW ] ) {
245 $options[ self::OPT_POOL_COUNTER ] = self::POOL_COUNTER_ARTICLE_VIEW;
246 $options[ self::OPT_POOL_COUNTER_FALLBACK ] = true;
247 }
248
249 $options += self::DEFAULT_OPTIONS;
250
251 $options['_normalized_'] = true;
252 return $options;
253 }
254
263 private function shouldUseCache(
264 PageRecord $page,
265 ?RevisionRecord $rev
266 ) {
267 if ( $rev && !$rev->getId() ) {
268 // The revision isn't from the database, so the output can't safely be cached.
269 return self::CACHE_NONE;
270 }
271
272 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
273 // NOTE: when we allow caching of old revisions in the future,
274 // we must not allow caching of deleted revisions.
275
276 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
277 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
278 return self::CACHE_NONE;
279 }
280
281 $isOld = $rev && $rev->getId() !== $page->getLatest();
282 if ( !$isOld ) {
283 return self::CACHE_PRIMARY;
284 }
285
286 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
287 // deleted/suppressed revision
288 return self::CACHE_NONE;
289 }
290
291 return self::CACHE_SECONDARY;
292 }
293
305 public function getCachedParserOutput(
306 PageRecord $page,
307 ParserOptions $parserOptions,
308 ?RevisionRecord $revision = null,
309 $options = []
310 ): ?ParserOutput {
311 $options = self::normalizeOptions( $options );
312
313 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
314 $isOld = $revision && $revision->getId() !== $page->getLatest();
315 $useCache = $this->shouldUseCache( $page, $revision );
316 $primaryCache = $this->getPrimaryCache( $parserOptions );
317 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
318
319 if ( $useCache === self::CACHE_PRIMARY ) {
320 if ( !$isOld && $this->localCache->hasField( $classCacheKey, $page->getLatest() ) ) {
321 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
322 }
323 $output = $primaryCache->get( $page, $parserOptions );
324 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
325 $secondaryCache = $this->getSecondaryCache( $parserOptions );
326 $output = $secondaryCache->get( $revision, $parserOptions );
327 } else {
328 $output = null;
329 }
330
331 $statType = $statReason = $output ? 'hit' : 'miss';
332
333 if (
334 $output && !$options[ self::OPT_IGNORE_PROFILE_VERSION ] &&
335 $output->getContentHolder()->isParsoidContent()
336 ) {
337 $pageBundle = $output->getContentHolder()->getBasePageBundle();
338 // T333606: Force a reparse if the version coming from cache is not the default
339 $cachedVersion = $pageBundle->version ?? null;
340 if (
341 $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing
342 $cachedVersion !== Parsoid::defaultHTMLVersion()
343 ) {
344 $statType = 'miss';
345 $statReason = 'obsolete';
346 $output = null;
347 }
348 }
349
350 if ( $output && !$isOld && !$parserOptions->getPostproc() ) {
351 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
352 }
353
354 $this->statsFactory
355 ->getCounter( 'parseroutputaccess_cache_total' )
356 ->setLabel( 'cache', $useCache )
357 ->setLabel( 'reason', $statReason )
358 ->setLabel( 'type', $statType )
359 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
360 ->increment();
361
362 return $output ?: null; // convert false to null
363 }
364
371 private function getFallbackOutputForLatest(
372 PageRecord $page,
373 ParserOptions $parserOptions,
374 string $workKey,
375 bool $fast
376 ) {
377 $parserOutput = $this->getPrimaryCache( $parserOptions )
378 ->getDirty( $page, $parserOptions );
379
380 if ( !$parserOutput ) {
381 $this->logger->info( 'dirty missing' );
382 return false;
383 }
384
385 if ( $fast ) {
386 // If this user recently made DB changes, then don't eagerly serve stale output,
387 // so that users generally see their own edits after page save.
388 //
389 // If PoolCounter is overloaded, we may end up here a second time (with fast=false),
390 // in which case we will serve a stale fallback then.
391 //
392 // Note that CP reports anything in the last 10 seconds from the same client,
393 // including to other pages and other databases, so we bias towards avoiding
394 // fast-stale responses for several seconds after saving an edit.
395 if ( $this->chronologyProtector->getTouched() ) {
396 $this->logger->info(
397 'declining fast-fallback to stale output since ChronologyProtector ' .
398 'reports the client recently made changes',
399 [ 'workKey' => $workKey ]
400 );
401 // Forget this ParserOutput -- we will request it again if
402 // necessary in slow mode. There might be a newer entry
403 // available by that time.
404 return false;
405 }
406 }
407
408 $this->logger->info( $fast ? 'fast dirty output' : 'dirty output', [ 'workKey' => $workKey ] );
409
410 $status = Status::newGood( $parserOutput );
411 $status->warning( 'view-pool-dirty-output' );
412 $status->warning( $fast ? 'view-pool-contention' : 'view-pool-overload' );
413 return $status;
414 }
415
440 public function getParserOutput(
441 PageRecord $page,
442 ParserOptions $parserOptions,
443 ?RevisionRecord $revision = null,
444 $options = []
445 ): Status {
446 $options = self::normalizeOptions( $options );
447
448 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
449 $error = $this->checkPreconditions( $page, $revision, $options );
450 if ( $error ) {
451 $this->statsFactory
452 ->getCounter( 'parseroutputaccess_case' )
453 ->setLabel( 'case', 'error' )
454 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
455 ->increment();
456 return $error;
457 }
458
459 $isOld = $revision && $revision->getId() !== $page->getLatest();
460 if ( $isOld ) {
461 $this->statsFactory
462 ->getCounter( 'parseroutputaccess_case' )
463 ->setLabel( 'case', 'old' )
464 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
465 ->increment();
466 } else {
467 $this->statsFactory
468 ->getCounter( 'parseroutputaccess_case' )
469 ->setLabel( 'case', 'current' )
470 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
471 ->increment();
472 }
473
474 if ( $this->shouldCheckCache( $parserOptions, $options ) ) {
475 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
476 if ( $output ) {
477 return Status::newGood( $output );
478 }
479 }
480
481 if ( !$revision ) {
482 $revId = $page->getLatest();
483 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
484
485 if ( !$revision ) {
486 $this->statsFactory
487 ->getCounter( 'parseroutputaccess_status' )
488 ->setLabel( 'status', 'norev' )
489 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
490 ->increment();
491 return Status::newFatal( 'missing-revision', $revId );
492 }
493 }
494
495 if ( $options[ self::OPT_POOL_COUNTER ] ) {
496 $work = $this->newPoolWork( $page, $parserOptions, $revision, $options );
498 $status = $work->execute();
499 } else {
500 // XXX: we could try harder to reuse a cache lookup above to
501 // provide the $previous argument here
502 $this->statsFactory->getCounter( 'parseroutputaccess_render_total' )
503 ->setLabel( 'pool', 'none' )
504 ->setLabel( 'cache', self::CACHE_NONE )
505 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
506 ->increment();
507
508 $status = $this->renderRevision( $page, $parserOptions, $revision, $options, null );
509 }
510
511 $output = $status->getValue();
512 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
513
514 // T301310: cache even uncacheable content locally
515 // T348255: temporarily disable local cache of postprocessed
516 // content out of an abundance of caution
517 if ( $output && !$isOld && !$parserOptions->getPostproc() ) {
518 $primaryCache = $this->getPrimaryCache( $parserOptions );
519 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
520 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
521 }
522
523 $labels = [
524 'postproc' => $parserOptions->getPostproc() ? 'true' : 'false',
525 'status' => $status->isGood() ? 'good' : ( $status->isOK() ? 'ok' : 'error' ),
526 ];
527
528 $this->statsFactory->getCounter( 'parseroutputaccess_status' )
529 ->setLabels( $labels )
530 ->increment();
531
532 return $status;
533 }
534
550 private function renderRevision(
551 PageRecord $page,
552 ParserOptions $parserOptions,
553 RevisionRecord $revision,
554 array $options,
555 ?ParserOutput $previousOutput = null
556 ): Status {
557 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
558
559 $isCurrent = $revision->getId() === $page->getLatest();
560
561 // T371713: Temporary statistics collection code to determine
562 // feasibility of Parsoid selective update
563 $sampleRate = $this->config->get(
564 MainConfigNames::ParsoidSelectiveUpdateSampleRate
565 );
566 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
567
568 if ( $previousOutput === null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
569 // If $useCache === self::CACHE_SECONDARY we could potentially
570 // try to reuse the parse of $revision-1 from the secondary cache,
571 // but it is likely those template transclusions are out of date.
572 // Try to reuse the template transclusions from the most recent
573 // parse, which are more likely to reflect the current template.
574 if ( $this->shouldCheckCache( $parserOptions, $options ) ) {
575 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?: null;
576 }
577 }
578
579 $preStatus = null;
580 if ( $parserOptions->getPostproc() ) {
581 $preParserOptions = $parserOptions->clearPostproc();
582 $preStatus = $this->getParserOutput( $page, $preParserOptions, $revision, $options );
583 $output = $preStatus->getValue();
584 if ( $output ) {
585 $output = $this->postprocess( $output, $parserOptions, $page, $revision );
586 }
587 } else {
588 $renderedRev = $this->revisionRenderer->getRenderedRevision( $revision, $parserOptions, null, [
589 'audience' => RevisionRecord::RAW,
590 'previous-output' => $previousOutput,
591 ] );
592
593 $output = $renderedRev->getRevisionParserOutput();
594 }
595
596 if ( $doSample ) {
597 # Keep these labels in sync with those in RefreshLinksJob
598 $labels = [
599 'source' => 'ParserOutputAccess',
600 'type' => $previousOutput === null ? 'full' : 'selective',
601 'reason' => $parserOptions->getRenderReason(),
602 'parser' => $parserOptions->getUseParsoid() ? 'parsoid' : 'legacy',
603 'opportunistic' => 'false',
604 'wiki' => WikiMap::getCurrentWikiId(),
605 'model' => $revision->getMainContentModel(),
606 'postproc' => $parserOptions->getPostproc() ? 'true' : 'false',
607 ];
608 $this->statsFactory
609 ->getCounter( 'ParserCache_selective_total' )
610 ->setLabels( $labels )
611 ->increment();
612 $this->statsFactory
613 ->getCounter( 'ParserCache_selective_cpu_seconds' )
614 ->setLabels( $labels )
615 ->incrementBy( $output->getTimeProfile( 'cpu' ) ?? 0 );
616 }
617
618 $res = Status::newGood( $output );
619 if ( $preStatus ) {
620 $res->merge( $preStatus );
621 }
622
623 if ( $output && $res->isGood() ) {
624 // do not cache the result if the parsercache result wasn't good (e.g. stale)
625 $this->saveToCache( $parserOptions, $output, $page, $revision, $options );
626 }
627
628 if ( $output && $options[ self::OPT_LINKS_UPDATE ] && !$parserOptions->getPostproc() ) {
629 $this->wikiPageFactory->newFromTitle( $page )
630 ->triggerOpportunisticLinksUpdate( $output );
631 }
632
633 return $res;
634 }
635
636 private function checkPreconditions(
637 PageRecord $page,
638 ?RevisionRecord $revision = null,
639 array $options = []
640 ): ?Status {
641 if ( !$page->exists() ) {
642 return Status::newFatal( 'nopagetext' );
643 }
644
645 if ( !$options[ self::OPT_NO_UPDATE_CACHE ] && $revision && !$revision->getId() ) {
646 throw new InvalidArgumentException(
647 'The revision does not have a known ID. Use OPT_NO_CACHE.'
648 );
649 }
650
651 if ( $revision && $revision->getPageId() !== $page->getId() ) {
652 throw new InvalidArgumentException(
653 'The revision does not belong to the given page.'
654 );
655 }
656
657 if ( $revision && !$options[ self::OPT_NO_AUDIENCE_CHECK ] ) {
658 // NOTE: If per-user checks are desired, the caller should perform them and
659 // then set OPT_NO_AUDIENCE_CHECK if they passed.
660 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
661 return Status::newFatal(
662 'missing-revision-permission',
663 $revision->getId(),
664 $revision->getTimestamp(),
665 $this->titleFormatter->getPrefixedURL( $page )
666 );
667 }
668 }
669
670 return null;
671 }
672
673 protected function newPoolWork(
674 PageRecord $page,
675 ParserOptions $parserOptions,
676 RevisionRecord $revision,
677 array $options
678 ): PoolCounterWork {
679 $profile = $options[ self::OPT_POOL_COUNTER ];
680 // Once we're in a pool counter, don't spawn another poolcounter job
681 $options[self::OPT_POOL_COUNTER] = false;
682 // Default behavior (no caching)
683 $callbacks = [
684 'doWork' => fn () => $this->renderRevision(
685 $page,
686 $parserOptions,
687 $revision,
688 $options
689 ),
690 // uncached
691 'doCachedWork' => static fn () => false,
692 // no fallback
693 'fallback' => static fn ( $fast ) => false,
694 'error' => static fn ( $status ) => $status,
695 ];
696
697 $useCache = $this->shouldUseCache( $page, $revision );
698
699 $this->statsFactory->getCounter( 'parseroutputaccess_render_total' )
700 ->setLabel( 'pool', 'articleview' )
701 ->setLabel( 'cache', $useCache )
702 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
703 ->increment();
704
705 switch ( $useCache ) {
706 case self::CACHE_PRIMARY:
707 $primaryCache = $this->getPrimaryCache( $parserOptions );
708 $parserCacheMetadata = $primaryCache->getMetadata( $page );
709 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
710 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
711 );
712
713 $workKey = $cacheKey . ':revid:' . $revision->getId();
714
715 $callbacks['doCachedWork'] =
716 static function () use ( $primaryCache, $page, $parserOptions ) {
717 $parserOutput = $primaryCache->get( $page, $parserOptions );
718 return $parserOutput ? Status::newGood( $parserOutput ) : false;
719 };
720
721 $callbacks['fallback'] =
722 function ( $fast ) use ( $page, $parserOptions, $workKey, $options ) {
723 if ( $options[ self::OPT_POOL_COUNTER_FALLBACK ] ) {
724 return $this->getFallbackOutputForLatest(
725 $page, $parserOptions, $workKey, $fast
726 );
727 } else {
728 return false;
729 }
730 };
731
732 break;
733
734 case self::CACHE_SECONDARY:
735 $secondaryCache = $this->getSecondaryCache( $parserOptions );
736 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
737
738 $callbacks['doCachedWork'] =
739 static function () use ( $secondaryCache, $revision, $parserOptions ) {
740 $parserOutput = $secondaryCache->get( $revision, $parserOptions );
741
742 return $parserOutput ? Status::newGood( $parserOutput ) : false;
743 };
744
745 break;
746
747 default:
748 $secondaryCache = $this->getSecondaryCache( $parserOptions );
749 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
750 }
751
752 $pool = $this->poolCounterFactory->create( $profile, $workKey );
753 return new PoolCounterWorkViaCallback(
754 $pool,
755 $workKey,
756 $callbacks
757 );
758 }
759
760 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
761 $name = $pOpts->getUseParsoid() ? self::PARSOID_PCACHE_NAME : ParserCacheFactory::DEFAULT_NAME;
762 if ( $pOpts->getPostproc() ) {
763 $name = self::POSTPROC_CACHE_PREFIX . $name;
764 }
765 return $this->parserCacheFactory->getParserCache( $name );
766 }
767
768 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
769 $name = $pOpts->getUseParsoid() ? self::PARSOID_RCACHE_NAME : ParserCacheFactory::DEFAULT_RCACHE_NAME;
770 if ( $pOpts->getPostproc() ) {
771 $name = self::POSTPROC_CACHE_PREFIX . $name;
772 }
773 return $this->parserCacheFactory->getRevisionOutputCache( $name );
774 }
775
776 private function startOperationSpan(
777 string $opName,
778 PageRecord $page,
779 ?RevisionRecord $revision = null
780 ): SpanInterface {
781 $span = $this->tracer->createSpan( "ParserOutputAccess::$opName" );
782 if ( $span->getContext()->isSampled() ) {
783 $span->setAttributes( [
784 'org.wikimedia.parser.page' => $page->__toString(),
785 'org.wikimedia.parser.page.id' => $page->getId(),
786 'org.wikimedia.parser.page.wiki' => $page->getWikiId(),
787 ] );
788 if ( $revision ) {
789 $span->setAttributes( [
790 'org.wikimedia.parser.revision.id' => $revision->getId(),
791 'org.wikimedia.parser.revision.parent_id' => $revision->getParentId(),
792 ] );
793 }
794 }
795 return $span->start()->activate();
796 }
797
802 public function clearLocalCache() {
803 $this->localCache->clear();
804 }
805
806 private function saveToCache(
807 ParserOptions $parserOptions, ParserOutput $output, PageRecord $page, RevisionRecord $revision, array $options
808 ): void {
809 $useCache = $this->shouldUseCache( $page, $revision );
810 if ( !$options[ self::OPT_NO_UPDATE_CACHE ] && $output->isCacheable() ) {
811 if ( $useCache === self::CACHE_PRIMARY ) {
812 $primaryCache = $this->getPrimaryCache( $parserOptions );
813 $primaryCache->save( $output, $page, $parserOptions );
814 } elseif ( $useCache === self::CACHE_SECONDARY ) {
815 $secondaryCache = $this->getSecondaryCache( $parserOptions );
816 $secondaryCache->save( $output, $revision, $parserOptions );
817 }
818 }
819 }
820
825 public function postprocess(
826 ParserOutput $output, ParserOptions $parserOptions,
827 PageRecord $page, ?RevisionRecord $revision = null
828 ): ParserOutput {
829 $useCache = $this->shouldUseCache( $page, $revision );
830 return self::postprocessInPipeline(
831 $this->outputTransformPipeline, $output, $parserOptions, $page,
832 fn ( $used ) => match ( $useCache ) {
833 self::CACHE_NONE => null,
834 self::CACHE_PRIMARY =>
835 $this->getPrimaryCache( $parserOptions )
836 ->makeParserOutputKey( $page, $parserOptions, $used ),
837 self::CACHE_SECONDARY =>
838 $this->getSecondaryCache( $parserOptions )
839 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable $revision is non-null here
840 ->makeParserOutputKey( $revision, $parserOptions, $used ),
841 }
842 );
843 }
844
861 public static function postprocessInPipeline(
862 OutputTransformPipeline $outputTransformPipeline,
863 ParserOutput $output, ParserOptions $parserOptions, PageRecord $page,
864 callable $getCacheKey
865 ): ParserOutput {
866 // Kludgey workaround: extract $textOptions from the $parserOptions
867 $textOptions = [];
868 // Don't add these to the used options set of $output because we
869 // don't want to mutate that, and the actual return value ParserOutput
870 // doesn't yet exist.
871 $parserOptions->registerWatcher( null );
872 foreach ( ParserOptions::$postprocOptions as $key ) {
873 $textOptions[$key] = $parserOptions->getOption( $key );
874 }
875 $textOptions = [
876 'allowClone' => true,
877 ] + $textOptions;
878
879 $output = $outputTransformPipeline->run( $output, $parserOptions, $textOptions );
880 // Ensure this ParserOptions is watching the resulting ParserOutput,
881 // now that it exists.
882 $parserOptions->registerWatcher( $output->recordOption( ... ) );
883 // Ensure "postproc" is in the set of used options
884 // (Probably not necessary, but it doesn't hurt to be safe.)
885 $parserOptions->getPostproc();
886 // Ensure all postprocOptions are in the set of used options
887 // (Since we can't detect accesses via $textOptions)
888 foreach ( ParserOptions::$postprocOptions as $key ) {
889 $parserOptions->getOption( $key );
890 }
891 // Add a cache message if debug info is requested (this used to
892 // be part of $textOptions)
893 $keyForDebugInfo = $parserOptions->getOption( 'includeDebugInfo' ) ?
894 $getCacheKey( $output->getUsedOptions() ) : null;
895 if ( $keyForDebugInfo !== null ) {
896 # Note that we can't make the key before postprocessing because
897 # the set of used options may vary during postprocessing; similarly
898 # we can't use ParserOutput::addCacheMsg() because the
899 # RenderDebugInfo stage has already run by the time we get here.
900 # So add the debug info "the hard way", but consistent with how
901 # RenderDebugInfo does it.
902 $timestamp = MWTimestamp::now();
903 $msg = "Post-processing cache key $keyForDebugInfo, generated at $timestamp";
904 // Sanitize for comment. Note '‐' in the replacement is U+2010,
905 // which looks much like the problematic '-'.
906 $msg = str_replace( [ '-', '>' ], [ '‐', '&gt;' ], $msg );
907 $output->setContentHolderText(
908 $output->getContentHolderText() . "<!--\n$msg\n-->"
909 );
910 }
911 return $output;
912 }
913
914 private function shouldCheckCache( ParserOptions $parserOptions, array $options ): bool {
915 if ( $options[ self::OPT_NO_CHECK_CACHE ] ) {
916 return false;
917 }
918 if ( $parserOptions->getPostproc() ) {
919 return !$options[ self::OPT_NO_POSTPROC_CACHE ];
920 }
921 return true;
922 }
923}
const CACHE_NONE
Definition Defines.php:73
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
A class containing constants representing the names of configuration variables.
run(ParserOutput $in, ParserOptions $popts, array $options)
Runs the pipeline on the ParserOutput, yielding a transformed ParserOutput.
Service for getting rendered output of a given page.
const OPT_FOR_ARTICLE_VIEW
Apply page view semantics.
static postprocessInPipeline(OutputTransformPipeline $outputTransformPipeline, ParserOutput $output, ParserOptions $parserOptions, PageRecord $page, callable $getCacheKey)
Postprocess the given ParserOutput in the given pipeline.
clearLocalCache()
Clear the local cache.
getCachedParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, $options=[])
Get the rendered output for the given page if it is present in the cache.
postprocess(ParserOutput $output, ParserOptions $parserOptions, PageRecord $page, ?RevisionRecord $revision=null)
Postprocess the given ParserOutput.
__construct(private readonly Config $config, private readonly OutputTransformPipeline $outputTransformPipeline, ParserCacheFactory $parserCacheFactory, RevisionLookup $revisionLookup, RevisionRenderer $revisionRenderer, StatsFactory $statsFactory, ChronologyProtector $chronologyProtector, WikiPageFactory $wikiPageFactory, TitleFormatter $titleFormatter, TracerInterface $tracer, PoolCounterFactory $poolCounterFactory)
getParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, $options=[])
Returns the rendered output for the given page.
newPoolWork(PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, array $options)
const OPT_POOL_COUNTER_FALLBACK
Whether to fall back to using stale content when failing to get a poolcounter lock.
Service for creating WikiPage objects.
getUsedOptions()
Returns the options from its ParserOptions which have been taken into account to produce the output.
recordOption(string $option)
Tags a parser option for use in the cache key for this parser output.
Cache for ParserOutput objects corresponding to the latest page revisions.
Set options of the Parser.
getPostproc()
Returns usage of postprocessing (and splits the cache accordingly)
getOption( $name)
Fetch an option and track that is was accessed.
registerWatcher( $callback)
Registers a callback for tracking which ParserOptions which are used.
ParserOutput is a rendering of a Content object or a message.
getTimeProfile(string $clock)
Returns the time that elapsed between the most recent call to resetParseStartTime() and the first cal...
getContentHolderText()
Returns the body fragment text of the ParserOutput.
setContentHolderText(?string $text)
Sets the body fragment text of the ParserOutput.
getContentHolder()
Return the ContentHolder storing the HTML/DOM contents of this ParserOutput.
Cache for ParserOutput objects.
Convenience class for dealing with PoolCounter using callbacks.
Class for dealing with PoolCounters using class members.
Page revision base class.
getParentId( $wikiId=self::LOCAL)
Get parent revision ID (the original previous page revision).
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
A title formatter service for MediaWiki.
Library for creating and parsing MW-style timestamps.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Store key-value entries in a size-limited in-memory LRU cache.
Provide a given client with protection against visible database lag.
This is the primary interface for validating metrics definitions, caching defined metrics,...
Interface for configuration instances.
Definition Config.php:18
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
Service for looking up page revisions.
Represents an OpenTelemetry span, i.e.
Base interface for an OpenTelemetry tracer responsible for creating spans.