MediaWiki 1.43.1
ParserOutputAccess.php
Go to the documentation of this file.
1<?php
20namespace MediaWiki\Page;
21
22use InvalidArgumentException;
23use MapCacheLRU;
24use MediaWiki\Logger\Spi as LoggerSpi;
44use Wikimedia\Assert\Assert;
45use Wikimedia\Parsoid\Parsoid;
49
60
62 public const PARSOID_PCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_NAME;
63
65 public const PARSOID_RCACHE_NAME = 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME;
66
70 public const OPT_NO_CHECK_CACHE = 1;
71
73 public const OPT_FORCE_PARSE = self::OPT_NO_CHECK_CACHE;
74
78 public const OPT_NO_UPDATE_CACHE = 2;
79
85 public const OPT_NO_AUDIENCE_CHECK = 4;
86
91 public const OPT_NO_CACHE = self::OPT_NO_UPDATE_CACHE | self::OPT_NO_CHECK_CACHE;
92
97 public const OPT_LINKS_UPDATE = 8;
98
111 public const OPT_FOR_ARTICLE_VIEW = 16;
112
117 public const OPT_IGNORE_PROFILE_VERSION = 128;
118
120 private const CACHE_NONE = 'none';
121
123 private const CACHE_PRIMARY = 'primary';
124
126 private const CACHE_SECONDARY = 'secondary';
127
133 private MapCacheLRU $localCache;
134
135 private ParserCacheFactory $parserCacheFactory;
136 private RevisionLookup $revisionLookup;
137 private RevisionRenderer $revisionRenderer;
138 private StatsFactory $statsFactory;
139 private ILBFactory $lbFactory;
140 private ChronologyProtector $chronologyProtector;
141 private LoggerSpi $loggerSpi;
142 private WikiPageFactory $wikiPageFactory;
143 private TitleFormatter $titleFormatter;
144
145 public function __construct(
146 ParserCacheFactory $parserCacheFactory,
147 RevisionLookup $revisionLookup,
148 RevisionRenderer $revisionRenderer,
149 StatsFactory $statsFactory,
150 ILBFactory $lbFactory,
151 ChronologyProtector $chronologyProtector,
152 LoggerSpi $loggerSpi,
153 WikiPageFactory $wikiPageFactory,
154 TitleFormatter $titleFormatter
155 ) {
156 $this->parserCacheFactory = $parserCacheFactory;
157 $this->revisionLookup = $revisionLookup;
158 $this->revisionRenderer = $revisionRenderer;
159 $this->statsFactory = $statsFactory;
160 $this->lbFactory = $lbFactory;
161 $this->chronologyProtector = $chronologyProtector;
162 $this->loggerSpi = $loggerSpi;
163 $this->wikiPageFactory = $wikiPageFactory;
164 $this->titleFormatter = $titleFormatter;
165
166 $this->localCache = new MapCacheLRU( 10 );
167 }
168
177 private function shouldUseCache(
178 PageRecord $page,
179 ?RevisionRecord $rev
180 ) {
181 if ( $rev && !$rev->getId() ) {
182 // The revision isn't from the database, so the output can't safely be cached.
183 return self::CACHE_NONE;
184 }
185
186 // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache().
187 // NOTE: when we allow caching of old revisions in the future,
188 // we must not allow caching of deleted revisions.
189
190 $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
191 if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) {
192 return self::CACHE_NONE;
193 }
194
195 $isOld = $rev && $rev->getId() !== $page->getLatest();
196 if ( !$isOld ) {
197 return self::CACHE_PRIMARY;
198 }
199
200 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
201 // deleted/suppressed revision
202 return self::CACHE_NONE;
203 }
204
205 return self::CACHE_SECONDARY;
206 }
207
218 public function getCachedParserOutput(
219 PageRecord $page,
220 ParserOptions $parserOptions,
221 ?RevisionRecord $revision = null,
222 int $options = 0
223 ): ?ParserOutput {
224 $isOld = $revision && $revision->getId() !== $page->getLatest();
225 $useCache = $this->shouldUseCache( $page, $revision );
226 $primaryCache = $this->getPrimaryCache( $parserOptions );
227 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
228
229 if ( $useCache === self::CACHE_PRIMARY ) {
230 if ( $this->localCache->hasField( $classCacheKey, $page->getLatest() ) && !$isOld ) {
231 return $this->localCache->getField( $classCacheKey, $page->getLatest() );
232 }
233 $output = $primaryCache->get( $page, $parserOptions );
234 } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) {
235 $secondaryCache = $this->getSecondaryCache( $parserOptions );
236 $output = $secondaryCache->get( $revision, $parserOptions );
237 } else {
238 $output = null;
239 }
240
241 $notHitReason = 'miss';
242 if (
243 $output && !( $options & self::OPT_IGNORE_PROFILE_VERSION ) &&
244 $parserOptions->getUseParsoid()
245 ) {
246 $pageBundleData = $output->getExtensionData(
247 PageBundleParserOutputConverter::PARSOID_PAGE_BUNDLE_KEY
248 );
249 // T333606: Force a reparse if the version coming from cache is not the default
250 $cachedVersion = $pageBundleData['version'] ?? null;
251 if (
252 $cachedVersion !== null && // T325137: BadContentModel, no sense in reparsing
253 $cachedVersion !== Parsoid::defaultHTMLVersion()
254 ) {
255 $notHitReason = 'obsolete';
256 $output = null;
257 }
258 }
259
260 if ( $output && !$isOld ) {
261 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
262 }
263
264 if ( $output ) {
265 $this->statsFactory
266 ->getCounter( 'parseroutputaccess_cache' )
267 ->setLabel( 'cache', $useCache )
268 ->setLabel( 'reason', 'hit' )
269 ->setLabel( 'type', 'hit' )
270 ->copyToStatsdAt( "ParserOutputAccess.Cache.$useCache.hit" )
271 ->increment();
272 } else {
273 $this->statsFactory
274 ->getCounter( 'parseroutputaccess_cache' )
275 ->setLabel( 'reason', $notHitReason )
276 ->setLabel( 'cache', $useCache )
277 ->setLabel( 'type', 'miss' )
278 ->copyToStatsdAt( "ParserOutputAccess.Cache.$useCache.$notHitReason" )
279 ->increment();
280 }
281
282 return $output ?: null; // convert false to null
283 }
284
307 public function getParserOutput(
308 PageRecord $page,
309 ParserOptions $parserOptions,
310 ?RevisionRecord $revision = null,
311 int $options = 0
312 ): Status {
313 $error = $this->checkPreconditions( $page, $revision, $options );
314 if ( $error ) {
315 $this->statsFactory
316 ->getCounter( 'parseroutputaccess_case' )
317 ->setLabel( 'case', 'error' )
318 ->copyToStatsdAt( 'ParserOutputAccess.Case.error' )
319 ->increment();
320 return $error;
321 }
322
323 $isOld = $revision && $revision->getId() !== $page->getLatest();
324 if ( $isOld ) {
325 $this->statsFactory
326 ->getCounter( 'parseroutputaccess_case' )
327 ->setLabel( 'case', 'old' )
328 ->copyToStatsdAt( 'ParserOutputAccess.Case.old' )
329 ->increment();
330 } else {
331 $this->statsFactory
332 ->getCounter( 'parseroutputaccess_case' )
333 ->setLabel( 'case', 'current' )
334 ->copyToStatsdAt( 'ParserOutputAccess.Case.current' )
335 ->increment();
336 }
337
338 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
339 $output = $this->getCachedParserOutput( $page, $parserOptions, $revision );
340 if ( $output ) {
341 return Status::newGood( $output );
342 }
343 }
344
345 if ( !$revision ) {
346 $revId = $page->getLatest();
347 $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null;
348
349 if ( !$revision ) {
350 $this->statsFactory
351 ->getCounter( 'parseroutputaccess_status' )
352 ->setLabel( 'status', 'norev' )
353 ->copyToStatsdAt( "ParserOutputAccess.Status.norev" )
354 ->increment();
355 return Status::newFatal( 'missing-revision', $revId );
356 }
357 }
358
359 if ( $options & self::OPT_FOR_ARTICLE_VIEW ) {
360 $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options );
362 $status = $work->execute();
363 } else {
364 // XXX: we could try harder to reuse a cache lookup above to
365 // provide the $previous argument here
366 $status = $this->renderRevision( $page, $parserOptions, $revision, $options, null );
367 }
368
369 $output = $status->getValue();
370 Assert::postcondition( $output || !$status->isOK(), 'Inconsistent status' );
371
372 if ( $output && !$isOld ) {
373 $primaryCache = $this->getPrimaryCache( $parserOptions );
374 $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions );
375 $this->localCache->setField( $classCacheKey, $page->getLatest(), $output );
376 }
377
378 if ( $status->isGood() ) {
379 $this->statsFactory->getCounter( 'parseroutputaccess_status' )
380 ->setLabel( 'status', 'good' )
381 ->copyToStatsdAt( 'ParserOutputAccess.Status.good' )
382 ->increment();
383 } elseif ( $status->isOK() ) {
384 $this->statsFactory->getCounter( 'parseroutputaccess_status' )
385 ->setLabel( 'status', 'ok' )
386 ->copyToStatsdAt( 'ParserOutputAccess.Status.ok' )
387 ->increment();
388 } else {
389 $this->statsFactory->getCounter( 'parseroutputaccess_status' )
390 ->setLabel( 'status', 'error' )
391 ->copyToStatsdAt( 'ParserOutputAccess.Status.error' )
392 ->increment();
393 }
394
395 return $status;
396 }
397
412 private function renderRevision(
413 PageRecord $page,
414 ParserOptions $parserOptions,
415 RevisionRecord $revision,
416 int $options,
417 ?ParserOutput $previousOutput = null
418 ): Status {
419 $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' )
420 ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.None' )
421 ->setLabel( 'cache', self::CACHE_NONE )
422 ->increment();
423
424 $useCache = $this->shouldUseCache( $page, $revision );
425
426 // T371713: Temporary statistics collection code to determine
427 // feasibility of Parsoid selective update
428 $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get(
429 MainConfigNames::ParsoidSelectiveUpdateSampleRate
430 );
431 $doSample = ( $sampleRate && mt_rand( 1, $sampleRate ) === 1 );
432
433 if ( $previousOutput === null && ( $doSample || $parserOptions->getUseParsoid() ) ) {
434 // If $useCache === self::CACHE_SECONDARY we could potentially
435 // try to reuse the parse of $revision-1 from the secondary cache,
436 // but it is likely those template transclusions are out of date.
437 // Try to reuse the template transclusions from the most recent
438 // parse, which are more likely to reflect the current template.
439 if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) {
440 $previousOutput = $this->getPrimaryCache( $parserOptions )->getDirty( $page, $parserOptions ) ?: null;
441 }
442 }
443
444 $renderedRev = $this->revisionRenderer->getRenderedRevision(
445 $revision,
446 $parserOptions,
447 null,
448 [
449 'audience' => RevisionRecord::RAW,
450 'previous-output' => $previousOutput,
451 ]
452 );
453
454 $output = $renderedRev->getRevisionParserOutput();
455
456 if ( $doSample ) {
457 $content = $revision->getContent( SlotRecord::MAIN );
458 $labels = [
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',
466 ];
467 $this->statsFactory
468 ->getCounter( 'ParserCache_selective_total' )
469 ->setLabels( $labels )
470 ->increment();
471 $this->statsFactory
472 ->getCounter( 'ParserCache_selective_cpu_seconds' )
473 ->setLabels( $labels )
474 ->incrementBy( $output->getTimeProfile( 'cpu' ) );
475 }
476
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 );
484 }
485 }
486
487 if ( $options & self::OPT_LINKS_UPDATE ) {
488 $this->wikiPageFactory->newFromTitle( $page )
489 ->triggerOpportunisticLinksUpdate( $output );
490 }
491
492 return Status::newGood( $output );
493 }
494
502 private function checkPreconditions(
503 PageRecord $page,
504 ?RevisionRecord $revision = null,
505 int $options = 0
506 ): ?Status {
507 if ( !$page->exists() ) {
508 return Status::newFatal( 'nopagetext' );
509 }
510
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.'
514 );
515 }
516
517 if ( $revision && $revision->getPageId() !== $page->getId() ) {
518 throw new InvalidArgumentException(
519 'The revision does not belong to the given page.'
520 );
521 }
522
523 if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) {
524 // NOTE: If per-user checks are desired, the caller should perform them and
525 // then set OPT_NO_AUDIENCE_CHECK if they passed.
526 if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) {
527 return Status::newFatal(
528 'missing-revision-permission',
529 $revision->getId(),
530 $revision->getTimestamp(),
531 $this->titleFormatter->getPrefixedDBkey( $page )
532 );
533 }
534 }
535
536 return null;
537 }
538
547 protected function newPoolWorkArticleView(
548 PageRecord $page,
549 ParserOptions $parserOptions,
550 RevisionRecord $revision,
551 int $options
552 ): PoolCounterWork {
553 $useCache = $this->shouldUseCache( $page, $revision );
554
555 switch ( $useCache ) {
556 case self::CACHE_PRIMARY:
557 $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' )
558 ->setLabel( 'cache', self::CACHE_PRIMARY )
559 ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Current' )
560 ->increment();
561 $primaryCache = $this->getPrimaryCache( $parserOptions );
562 $parserCacheMetadata = $primaryCache->getMetadata( $page );
563 $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions,
564 $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null
565 );
566
567 $workKey = $cacheKey . ':revid:' . $revision->getId();
568
570 $workKey,
571 $page,
572 $revision,
573 $parserOptions,
574 $this->revisionRenderer,
575 $primaryCache,
576 $this->lbFactory,
577 $this->chronologyProtector,
578 $this->loggerSpi,
579 $this->wikiPageFactory,
580 !( $options & self::OPT_NO_UPDATE_CACHE ),
581 (bool)( $options & self::OPT_LINKS_UPDATE )
582 );
583
584 case self::CACHE_SECONDARY:
585 $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' )
586 ->setLabel( 'cache', self::CACHE_SECONDARY )
587 ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Old' )
588 ->increment();
589 $secondaryCache = $this->getSecondaryCache( $parserOptions );
590 $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions );
591 return new PoolWorkArticleViewOld(
592 $workKey,
593 $secondaryCache,
594 $revision,
595 $parserOptions,
596 $this->revisionRenderer,
597 $this->loggerSpi
598 );
599
600 default:
601 $this->statsFactory->getCounter( 'parseroutputaccess_poolwork' )
602 ->setLabel( 'cache', self::CACHE_NONE )
603 ->copyToStatsdAt( 'ParserOutputAccess.PoolWork.Uncached' )
604 ->increment();
605 $secondaryCache = $this->getSecondaryCache( $parserOptions );
606 $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions );
607 return new PoolWorkArticleView(
608 $workKey,
609 $revision,
610 $parserOptions,
611 $this->revisionRenderer,
612 $this->loggerSpi
613 );
614 }
615
616 // unreachable
617 }
618
619 private function getPrimaryCache( ParserOptions $pOpts ): ParserCache {
620 if ( $pOpts->getUseParsoid() ) {
621 return $this->parserCacheFactory->getParserCache(
622 self::PARSOID_PCACHE_NAME
623 );
624 }
625
626 return $this->parserCacheFactory->getParserCache(
627 ParserCacheFactory::DEFAULT_NAME
628 );
629 }
630
631 private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache {
632 if ( $pOpts->getUseParsoid() ) {
633 return $this->parserCacheFactory->getRevisionOutputCache(
634 self::PARSOID_RCACHE_NAME
635 );
636 }
637
638 return $this->parserCacheFactory->getRevisionOutputCache(
639 ParserCacheFactory::DEFAULT_RCACHE_NAME
640 );
641 }
642
643}
const CACHE_NONE
Definition Defines.php:87
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Store key-value entries in a size-limited in-memory LRU cache.
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.
__construct(ParserCacheFactory $parserCacheFactory, RevisionLookup $revisionLookup, RevisionRenderer $revisionRenderer, StatsFactory $statsFactory, ILBFactory $lbFactory, ChronologyProtector $chronologyProtector, LoggerSpi $loggerSpi, WikiPageFactory $wikiPageFactory, TitleFormatter $titleFormatter)
const OPT_FOR_ARTICLE_VIEW
Apply page view semantics.
getCachedParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page if it is present in the cache.
newPoolWorkArticleView(PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, int $options)
getParserOutput(PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision=null, int $options=0)
Returns the rendered output for the given page.
Service for creating WikiPage objects.
Cache for ParserOutput objects corresponding to the latest page revisions.
Set options of the Parser.
getUseParsoid()
Parsoid-format HTML output, or legacy wikitext parser HTML?
ParserOutput is a rendering of a Content object or a message.
Provides methods for conversion between PageBundle and ParserOutput TODO: Convert to a trait once we ...
Cache for ParserOutput objects.
Class for dealing with PoolCounters using class members.
PoolWorkArticleView for the current revision of a page, using ParserCache.
PoolWorkArticleView for an old revision of a page, using a simple cache.
PoolCounter protected work wrapping RenderedRevision->getRevisionParserOutput.
Page revision base class.
audienceCan( $field, $audience, ?Authority $performer=null)
Check that the given audience has access to the given field.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
Value object representing a content slot associated with a page revision.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:31
Provide a given client with protection against visible database lag.
StatsFactory Implementation.
Service provider interface to create \Psr\Log\LoggerInterface objects.
Definition Spi.php:64
exists()
Checks if the page currently exists.
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.
A title formatter service for MediaWiki.
Manager of ILoadBalancer objects and, indirectly, IDatabase connections.
setLabel(string $key, string $value)
copyToStatsdAt( $statsdNamespaces)