MediaWiki master
ParserOutputAccess.php
Go to the documentation of this file.
1<?php
6namespace MediaWiki\Page;
7
8use InvalidArgumentException;
25use Psr\Log\LoggerAwareInterface;
26use Psr\Log\LoggerInterface;
27use Psr\Log\NullLogger;
28use Wikimedia\Assert\Assert;
30use Wikimedia\Parsoid\Parsoid;
35
45class ParserOutputAccess implements LoggerAwareInterface {
46
48 public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME;
49
51 public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME;
52
54 public const POSTPROC_CACHE_PREFIX = 'postproc-';
55
59 public const OPT_NO_CHECK_CACHE = 1;
60
62 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
63
67 public const OPT_NO_UPDATE_CACHE = 2;
68
74 public const OPT_NO_AUDIENCE_CHECK = 4;
75
80 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
81
86 public const OPT_LINKS_UPDATE = 8;
87
102 public const OPT_FOR_ARTICLE_VIEW = 16;
103
108 public const OPT_IGNORE_PROFILE_VERSION = 128;
109
113 public const OPT_NO_POSTPROC_CACHE = 256;
114
119 public const OPT_POOL_COUNTER_FALLBACK = 'poolcounter-fallback';
120
124 public const OPT_POOL_COUNTER = 'poolcounter-type';
125
129 public const POOL_COUNTER_ARTICLE_VIEW = 'ArticleView';
130
134 public const POOL_COUNTER_REST_API = 'HtmlRestApi';
135
140 private const DEFAULT_OPTIONS = [
141 self::OPT_POOL_COUNTER => null,
142 self::OPT_POOL_COUNTER_FALLBACK => false
143 ];
144
146 private const CACHE_NONE = 'none';
147
149 private const CACHE_PRIMARY = 'primary';
150
152 private const CACHE_SECONDARY = 'secondary';
153
159 private MapCacheLRU $localCache;
160
161 private ParserCacheFactory $parserCacheFactory;
162 private RevisionLookup $revisionLookup;
163 private RevisionRenderer $revisionRenderer;
164 private StatsFactory $statsFactory;
165 private ChronologyProtector $chronologyProtector;
166 private WikiPageFactory $wikiPageFactory;
167 private TitleFormatter $titleFormatter;
168 private TracerInterface $tracer;
169 private PoolCounterFactory $poolCounterFactory;
170 private LoggerInterface $logger;
171
172 public function __construct(
173 ParserCacheFactory $parserCacheFactory,
174 RevisionLookup $revisionLookup,
175 RevisionRenderer $revisionRenderer,
176 StatsFactory $statsFactory,
177 ChronologyProtector $chronologyProtector,
178 WikiPageFactory $wikiPageFactory,
179 TitleFormatter $titleFormatter,
180 TracerInterface $tracer,
181 PoolCounterFactory $poolCounterFactory
182 ) {
183 $this->parserCacheFactory = $parserCacheFactory;
184 $this->revisionLookup = $revisionLookup;
185 $this->revisionRenderer = $revisionRenderer;
186 $this->statsFactory = $statsFactory;
187 $this->chronologyProtector = $chronologyProtector;
188 $this->wikiPageFactory = $wikiPageFactory;
189 $this->titleFormatter = $titleFormatter;
190 $this->tracer = $tracer;
191 $this->poolCounterFactory = $poolCounterFactory;
192
193 $this->localCache = new MapCacheLRU( 10 );
194 $this->logger = new NullLogger();
195 }
196
197 public function setLogger( LoggerInterface $logger ): void {
198 $this->logger = $logger;
199 }
200
212 private static function normalizeOptions( $options ): array {
213 $bits = 0;
214
215 // TODO: Starting in 1.46, emit deprecation warnings when getting an int.
216
217 if ( is_array( $options ) ) {
218 if ( $options['_normalized_'] ?? false ) {
219 // already normalized.
220 return $options;
221 }
222
223 // Collect all bits from array keys, in case one of the keys
224 // sets multiple bits.
225 foreach ( $options as $opt => $enabled ) {
226 if ( is_int( $opt ) && $enabled === true ) {
227 $bits |= $opt;
228 }
229 }
230 } else {
231 $bits = $options;
232 $options = [];
233 }
234
235 // From the (numerically) smallest to the largest option that can possibly exist
236 for ( $b = self::OPT_NO_CHECK_CACHE; $b <= self::OPT_NO_POSTPROC_CACHE; $b <<= 1 ) {
237 $options[$b] = (bool)( $bits & $b );
238 }
239
240 if ( $options[ self::OPT_FOR_ARTICLE_VIEW ] ) {
241 $options[ self::OPT_POOL_COUNTER ] = self::POOL_COUNTER_ARTICLE_VIEW;
242 $options[ self::OPT_POOL_COUNTER_FALLBACK ] = true;
243 }
244
245 $options += self::DEFAULT_OPTIONS;
246
247 $options['_normalized_'] = true;
248 return $options;
249 }
250
259 private function shouldUseCache(
260 PageRecord $page,
261 ?RevisionRecord $rev
262 ) {
263 if ( $rev && !$rev->getId() ) {
264 // The revision isn't from the database, so the output can't safely be cached.
265 return self::CACHE_NONE;
266 }
267
268 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
269 // NOTE: when we allow caching of old revisions in the future,
270 // we must not allow caching of deleted revisions.
271
272 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
273 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
274 return self::CACHE_NONE;
275 }
276
277 $isOld = $rev && $rev->getId() !== $page->getLatest();
278 if ( !$isOld ) {
279 return self::CACHE_PRIMARY;
280 }
281
282 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
283 // deleted/suppressed revision
284 return self::CACHE_NONE;
285 }
286
287 return self::CACHE_SECONDARY;
288 }
289
301 public function getCachedParserOutput(
302 PageRecord $page,
303 ParserOptions $parserOptions,
304 ?RevisionRecord $revision = null,
305 $options = []
306 ): ?ParserOutput {
307 $options = self::normalizeOptions( $options );
308
309 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
310 $isOld = $revision && $revision->getId() !== $page->getLatest();
311 $useCache = $this->shouldUseCache( $page, $revision );
312 $primaryCache = $this->getPrimaryCache( $parserOptions );
313 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
314
315 if ( $useCache === self::CACHE_PRIMARY ) {
316 if ( !$isOld && $this->localCache->hasField( $classCacheKey, $page->getLatest() ) ) {
317 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
318 }
319 $output = $primaryCache->get( $page, $parserOptions );
320 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
321 $secondaryCache = $this->getSecondaryCache( $parserOptions );
322 $output = $secondaryCache->get( $revision, $parserOptions );
323 } else {
324 $output = null;
325 }
326
327 $statType = $statReason = $output ? 'hit' : 'miss';
328
329 if (
330 $output && !$options[ self::OPT_IGNORE_PROFILE_VERSION ] &&
331 $output->getContentHolder()->isParsoidContent()
332 ) {
333 $pageBundle = $output->getContentHolder()->getBasePageBundle();
334 // T333606: Force a reparse if the version coming from cache is not the default
335 $cachedVersion = $pageBundle->version ?? null;
336 if (
337 $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing
338 $cachedVersion !== Parsoid::defaultHTMLVersion()
339 ) {
340 $statType = 'miss';
341 $statReason = 'obsolete';
342 $output = null;
343 }
344 }
345
346 if ( $output && !$isOld && !$parserOptions->getPostproc() ) {
347 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
348 }
349
350 $this->statsFactory
351 ->getCounter( 'parseroutputaccess_cache_total' )
352 ->setLabel( 'cache', $useCache )
353 ->setLabel( 'reason', $statReason )
354 ->setLabel( 'type', $statType )
355 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
356 ->increment();
357
358 return $output ?: null; // convert false to null
359 }
360
367 private function getFallbackOutputForLatest(
368 PageRecord $page,
369 ParserOptions $parserOptions,
370 string $workKey,
371 bool $fast
372 ) {
373 $parserOutput = $this->getPrimaryCache( $parserOptions )
374 ->getDirty( $page, $parserOptions );
375
376 if ( !$parserOutput ) {
377 $this->logger->info( 'dirty missing' );
378 return false;
379 }
380
381 if ( $fast ) {
382 // If this user recently made DB changes, then don't eagerly serve stale output,
383 // so that users generally see their own edits after page save.
384 //
385 // If PoolCounter is overloaded, we may end up here a second time (with fast=false),
386 // in which case we will serve a stale fallback then.
387 //
388 // Note that CP reports anything in the last 10 seconds from the same client,
389 // including to other pages and other databases, so we bias towards avoiding
390 // fast-stale responses for several seconds after saving an edit.
391 if ( $this->chronologyProtector->getTouched() ) {
392 $this->logger->info(
393 'declining fast-fallback to stale output since ChronologyProtector ' .
394 'reports the client recently made changes',
395 [ 'workKey' => $workKey ]
396 );
397 // Forget this ParserOutput -- we will request it again if
398 // necessary in slow mode. There might be a newer entry
399 // available by that time.
400 return false;
401 }
402 }
403
404 $this->logger->info( $fast ? 'fast dirty output' : 'dirty output', [ 'workKey' => $workKey ] );
405
406 $status = Status::newGood( $parserOutput );
407 $status->warning( 'view-pool-dirty-output' );
408 $status->warning( $fast ? 'view-pool-contention' : 'view-pool-overload' );
409 return $status;
410 }
411
436 public function getParserOutput(
437 PageRecord $page,
438 ParserOptions $parserOptions,
439 ?RevisionRecord $revision = null,
440 $options = []
441 ): Status {
442 $options = self::normalizeOptions( $options );
443
444 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
445 $error = $this->checkPreconditions( $page, $revision, $options );
446 if ( $error ) {
447 $this->statsFactory
448 ->getCounter( 'parseroutputaccess_case' )
449 ->setLabel( 'case', 'error' )
450 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
451 ->increment();
452 return $error;
453 }
454
455 $isOld = $revision && $revision->getId() !== $page->getLatest();
456 if ( $isOld ) {
457 $this->statsFactory
458 ->getCounter( 'parseroutputaccess_case' )
459 ->setLabel( 'case', 'old' )
460 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
461 ->increment();
462 } else {
463 $this->statsFactory
464 ->getCounter( 'parseroutputaccess_case' )
465 ->setLabel( 'case', 'current' )
466 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
467 ->increment();
468 }
469
470 if ( $this->shouldCheckCache( $parserOptions, $options ) ) {
471 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
472 if ( $output ) {
473 return Status::newGood( $output );
474 }
475 }
476
477 if ( !$revision ) {
478 $revId = $page->getLatest();
479 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
480
481 if ( !$revision ) {
482 $this->statsFactory
483 ->getCounter( 'parseroutputaccess_status' )
484 ->setLabel( 'status', 'norev' )
485 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
486 ->increment();
487 return Status::newFatal( 'missing-revision', $revId );
488 }
489 }
490
491 if ( $options[ self::OPT_POOL_COUNTER ] ) {
492 $work = $this->newPoolWork( $page, $parserOptions, $revision, $options );
494 $status = $work->execute();
495 } else {
496 // XXX: we could try harder to reuse a cache lookup above to
497 // provide the $previous argument here
498 $this->statsFactory->getCounter( 'parseroutputaccess_render_total' )
499 ->setLabel( 'pool', 'none' )
500 ->setLabel( 'cache', self::CACHE_NONE )
501 ->setLabel( 'postproc', $parserOptions->getPostproc() ? 'true' : 'false' )
502 ->increment();
503
504 $status = $this->renderRevision( $page, $parserOptions, $revision, $options, null );
505 }
506
507 $output = $status->getValue();
508 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
509
510 // T301310: cache even uncacheable content locally
511 // T348255: temporarily disable local cache of postprocessed
512 // content out of an abundance of caution
513 if ( $output && !$isOld && !$parserOptions->getPostproc() ) {
514 $primaryCache = $this->getPrimaryCache( $parserOptions );
515 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
516 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
517 }
518
519 $labels = [
520 'postproc' => $parserOptions->getPostproc() ? 'true' : 'false',
521 'status' => $status->isGood() ? 'good' : ( $status->isOK() ? 'ok' : 'error' ),
522 ];
523
524 $this->statsFactory->getCounter( 'parseroutputaccess_status' )
525 ->setLabels( $labels )
526 ->increment();
527
528 return $status;
529 }
530
546 private function renderRevision(
547 PageRecord $page,
548 ParserOptions $parserOptions,
549 RevisionRecord $revision,
550 array $options,
551 ?ParserOutput $previousOutput = null
552 ): Status {
553 $span = $this->startOperationSpan( __FUNCTION__, $page, $revision );
554
555 $isCurrent = $revision->getId() === $page->getLatest();
556
557 // T371713: Temporary statistics collection code to determine
558 // feasibility of Parsoid selective update
559 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
560 MainConfigNames::ParsoidSelectiveUpdateSampleRate
561 );
562 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
563
564 if ( $previousOutput === null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
565 // If $useCache === self::CACHE_SECONDARY we could potentially
566 // try to reuse the parse of $revision-1 from the secondary cache,
567 // but it is likely those template transclusions are out of date.
568 // Try to reuse the template transclusions from the most recent
569 // parse, which are more likely to reflect the current template.
570 if ( $this->shouldCheckCache( $parserOptions, $options ) ) {
571 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?: null;
572 }
573 }
574
575 $preStatus = null;
576 if ( $parserOptions->getPostproc() ) {
577 $preParserOptions = $parserOptions->clearPostproc();
578 $preStatus = $this->getParserOutput( $page, $preParserOptions, $revision, $options );
579 $output = $preStatus->getValue();
580 if ( $output ) {
581 $output = $this->postprocess( $output, $parserOptions );
582 }
583 } else {
584 $renderedRev = $this->revisionRenderer->getRenderedRevision( $revision, $parserOptions, null, [
585 'audience' => RevisionRecord::RAW,
586 'previous-output' => $previousOutput,
587 ] );
588
589 $output = $renderedRev->getRevisionParserOutput();
590 }
591
592 if ( $doSample ) {
593 $labels = [
594 'source' => 'ParserOutputAccess',
595 'type' => $previousOutput === null ? 'full' : 'selective',
596 'reason' => $parserOptions->getRenderReason(),
597 'parser' => $parserOptions->getUseParsoid() ? 'parsoid' : 'legacy',
598 'opportunistic' => 'false',
599 'wiki' => WikiMap::getCurrentWikiId(),
600 'model' => $revision->getMainContentModel(),
601 'postproc' => $parserOptions->getPostproc() ? 'true' : 'false',
602 ];
603 $this->statsFactory
604 ->getCounter( 'ParserCache_selective_total' )
605 ->setLabels( $labels )
606 ->increment();
607 $this->statsFactory
608 ->getCounter( 'ParserCache_selective_cpu_seconds' )
609 ->setLabels( $labels )
610 ->incrementBy( $output->getTimeProfile( 'cpu' ) ?? 0 );
611 }
612
613 $res = Status::newGood( $output );
614 if ( $preStatus ) {
615 $res->merge( $preStatus );
616 }
617
618 if ( $output && $res->isGood() ) {
619 // do not cache the result if the parsercache result wasn't good (e.g. stale)
620 $this->saveToCache( $parserOptions, $output, $page, $revision, $options );
621 }
622
623 if ( $output && $options[ self::OPT_LINKS_UPDATE ] && !$parserOptions->getPostproc() ) {
624 $this->wikiPageFactory->newFromTitle( $page )
625 ->triggerOpportunisticLinksUpdate( $output );
626 }
627
628 return $res;
629 }
630
631 private function checkPreconditions(
632 PageRecord $page,
633 ?RevisionRecord $revision = null,
634 array $options = []
635 ): ?Status {
636 if ( !$page->exists() ) {
637 return Status::newFatal( 'nopagetext' );
638 }
639
640 if ( !$options[ self::OPT_NO_UPDATE_CACHE ] && $revision && !$revision->getId() ) {
641 throw new InvalidArgumentException(
642 'The revision does not have a known ID. Use OPT_NO_CACHE.'
643 );
644 }
645
646 if ( $revision && $revision->getPageId() !== $page->getId() ) {
647 throw new InvalidArgumentException(
648 'The revision does not belong to the given page.'
649 );
650 }
651
652 if ( $revision && !$options[ self::OPT_NO_AUDIENCE_CHECK ] ) {
653 // NOTE: If per-user checks are desired, the caller should perform them and
654 // then set OPT_NO_AUDIENCE_CHECK if they passed.
655 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
656 return Status::newFatal(
657 'missing-revision-permission',
658 $revision->getId(),
659 $revision->getTimestamp(),
660 $this->titleFormatter->getPrefixedURL( $page )
661 );
662 }
663 }
664
665 return null;
666 }
667
668 protected function newPoolWork(
669 PageRecord $page,
670 ParserOptions $parserOptions,
671 RevisionRecord $revision,
672 array $options
673 ): PoolCounterWork {
674 // Default behavior (no caching)
675 $callbacks = [
676 'doWork' => function () use ( $page, $parserOptions, $revision, $options ) {
677 return $this->renderRevision(
678 $page,
679 $parserOptions,
680 $revision,
681 $options
682 );
683 },
684 'doCachedWork' => static function () {
685 // uncached
686 return false;
687 },
688 'fallback' => static function ( $fast ) {
689 // no fallback
690 return false;
691 },
692 'error' => static function ( $status ) {
693 return $status;
694 }
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 $profile = $options[ self::OPT_POOL_COUNTER ];
753 $pool = $this->poolCounterFactory->create( $profile, $workKey );
754 return new PoolCounterWorkViaCallback(
755 $pool,
756 $workKey,
757 $callbacks
758 );
759 }
760
761 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
762 $name = $pOpts->getUseParsoid() ? self::PARSOID_PCACHE_NAME : ParserCacheFactory::DEFAULT_NAME;
763 if ( $pOpts->getPostproc() ) {
764 $name = self::POSTPROC_CACHE_PREFIX . $name;
765 }
766 return $this->parserCacheFactory->getParserCache( $name );
767 }
768
769 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
770 $name = $pOpts->getUseParsoid() ? self::PARSOID_RCACHE_NAME : ParserCacheFactory::DEFAULT_RCACHE_NAME;
771 if ( $pOpts->getPostproc() ) {
772 $name = self::POSTPROC_CACHE_PREFIX . $name;
773 }
774 return $this->parserCacheFactory->getRevisionOutputCache( $name );
775 }
776
777 private function startOperationSpan(
778 string $opName,
779 PageRecord $page,
780 ?RevisionRecord $revision = null
781 ): SpanInterface {
782 $span = $this->tracer->createSpan( "ParserOutputAccess::$opName" );
783 if ( $span->getContext()->isSampled() ) {
784 $span->setAttributes( [
785 'org.wikimedia.parser.page' => $page->__toString(),
786 'org.wikimedia.parser.page.id' => $page->getId(),
787 'org.wikimedia.parser.page.wiki' => $page->getWikiId(),
788 ] );
789 if ( $revision ) {
790 $span->setAttributes( [
791 'org.wikimedia.parser.revision.id' => $revision->getId(),
792 'org.wikimedia.parser.revision.parent_id' => $revision->getParentId(),
793 ] );
794 }
795 }
796 return $span->start()->activate();
797 }
798
803 public function clearLocalCache() {
804 $this->localCache->clear();
805 }
806
807 private function saveToCache(
808 ParserOptions $parserOptions, ParserOutput $output, PageRecord $page, RevisionRecord $revision, array $options
809 ): void {
810 $useCache = $this->shouldUseCache( $page, $revision );
811 if ( !$options[ self::OPT_NO_UPDATE_CACHE ] && $output->isCacheable() ) {
812 if ( $useCache === self::CACHE_PRIMARY ) {
813 $primaryCache = $this->getPrimaryCache( $parserOptions );
814 $primaryCache->save( $output, $page, $parserOptions );
815 } elseif ( $useCache === self::CACHE_SECONDARY ) {
816 $secondaryCache = $this->getSecondaryCache( $parserOptions );
817 $secondaryCache->save( $output, $revision, $parserOptions );
818 }
819 }
820 }
821
825 public function postprocess( ParserOutput $output, ParserOptions $parserOptions ): ParserOutput {
826 // Kludgey workaround: extract $textOptions from the $parserOptions
827 $textOptions = [];
828 // Don't add these to the used options set of $output because we
829 // don't want to mutate that, and the actual return value ParserOutput
830 // doesn't yet exist.
831 $parserOptions->registerWatcher( null );
832 foreach ( ParserOptions::$postprocOptions as $key ) {
833 $textOptions[$key] = $parserOptions->getOption( $key );
834 }
835 $textOptions = [
836 'allowClone' => true,
837 ] + $textOptions;
838
839 $pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
840 $output = $pipeline->run( $output, $parserOptions, $textOptions );
841 // Ensure this ParserOptions is watching the resulting ParserOutput,
842 // now that it exists.
843 $parserOptions->registerWatcher( $output->recordOption( ... ) );
844 // Ensure "postproc" is in the set of used options
845 // (Probably not necessary, but it doesn't hurt to be safe.)
846 $parserOptions->getPostproc();
847 // Ensure all postprocOptions are in the set of used options
848 // (Since we can't detect accesses via $textOptions)
849 foreach ( ParserOptions::$postprocOptions as $key ) {
850 $parserOptions->getOption( $key );
851 }
852 return $output;
853 }
854
855 private function shouldCheckCache( ParserOptions $parserOptions, array $options ): bool {
856 if ( $options[ self::OPT_NO_CHECK_CACHE ] ) {
857 return false;
858 }
859 if ( $parserOptions->getPostproc() ) {
860 return !$options[ self::OPT_NO_POSTPROC_CACHE ];
861 }
862 return true;
863 }
864}
const CACHE_NONE
Definition Defines.php:73
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for getting rendered output of a given page.
const OPT_FOR_ARTICLE_VIEW
Apply page view semantics.
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)
Postprocess the given ParserOutput.
__construct(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.
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...
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.
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.
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,...
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.