MediaWiki  master
ParserCache.php
Go to the documentation of this file.
1 <?php
28 use Psr\Log\LoggerInterface;
29 
61 class ParserCache {
69  public const USE_CURRENT_ONLY = 0;
70 
72  public const USE_EXPIRED = 1;
73 
75  public const USE_OUTDATED = 2;
76 
81  private const USE_ANYTHING = 3;
82 
84  private $name;
85 
87  private $cache;
88 
94  private $cacheEpoch;
95 
97  private $hookRunner;
98 
100  private $jsonCodec;
101 
103  private $stats;
104 
106  private $logger;
107 
112  private $writeJson = false;
113 
118  private $readJson = false;
119 
135  public function __construct(
136  string $name,
138  string $cacheEpoch,
139  HookContainer $hookContainer,
142  LoggerInterface $logger,
143  $useJson = false
144  ) {
145  if ( !$cache instanceof EmptyBagOStuff && !$cache instanceof CachedBagOStuff ) {
146  // It seems on some page views, the same entry is retreived twice from the ParserCache.
147  // This shouldn't happen but use a process-cache and log duplicate fetches to mitigate
148  // this and figure out why. (T269593)
149  $cache = new CachedBagOStuff( $cache, [
150  'logger' => $logger,
151  'asyncHandler' => [ DeferredUpdates::class, 'addCallableUpdate' ],
152  'reportDupes' => true,
153  // Each ParserCache entry uses 2 keys, one for metadata and one for parser output.
154  // So, cache at most 4 different parser outputs in memory. The number was chosen ad hoc.
155  'maxKeys' => 8
156  ] );
157  }
158  $this->name = $name;
159  $this->cache = $cache;
160  $this->cacheEpoch = $cacheEpoch;
161  $this->hookRunner = new HookRunner( $hookContainer );
162  $this->jsonCodec = $jsonCodec;
163  $this->stats = $stats;
164  $this->logger = $logger;
165  $this->readJson = $useJson;
166  $this->writeJson = $useJson;
167  }
168 
173  public function deleteOptionsKey( WikiPage $wikiPage ) {
174  $this->cache->delete( $this->makeMetadataKey( $wikiPage ) );
175  }
176 
192  public function getETag( WikiPage $wikiPage, $popts ) {
193  wfDeprecated( __METHOD__, '1.36' );
194  return 'W/"' . $this->makeParserOutputKey( $wikiPage, $popts )
195  . "--" . $wikiPage->getTouched() . '"';
196  }
197 
204  public function getDirty( WikiPage $wikiPage, $popts ) {
205  $value = $this->get( $wikiPage, $popts, true );
206  return is_object( $value ) ? $value : false;
207  }
208 
213  private function incrementStats( WikiPage $wikiPage, $metricSuffix ) {
214  $contentModel = str_replace( '.', '_', $wikiPage->getContentModel() );
215  $metricSuffix = str_replace( '.', '_', $metricSuffix );
216  $this->stats->increment( "{$this->name}.{$contentModel}.{$metricSuffix}" );
217  }
218 
240  public function getKey( WikiPage $wikiPage, $popts, $useOutdated = self::USE_ANYTHING ) {
241  wfDeprecated( __METHOD__, '1.36' );
242  if ( is_bool( $useOutdated ) ) {
243  $useOutdated = $useOutdated ? self::USE_ANYTHING : self::USE_CURRENT_ONLY;
244  }
245 
246  if ( $popts instanceof User ) {
247  $this->logger->warning(
248  "Use of outdated prototype ParserCache::getKey( &\$wikiPage, &\$user )\n"
249  );
250  $popts = ParserOptions::newFromUser( $popts );
251  }
252 
253  $metadata = $this->getMetadata( $wikiPage, $useOutdated );
254  if ( !$metadata ) {
255  if ( $useOutdated < self::USE_ANYTHING ) {
256  return false;
257  }
258  $usedOptions = ParserOptions::allCacheVaryingOptions();
259  } else {
260  $usedOptions = $metadata->getUsedOptions();
261  }
262 
263  return $this->makeParserOutputKey( $wikiPage, $popts, $usedOptions );
264  }
265 
279  public function getMetadata(
280  WikiPage $wikiPage,
281  int $staleConstraint = self::USE_ANYTHING
282  ): ?ParserCacheMetadata {
283  $pageKey = $this->makeMetadataKey( $wikiPage );
284  $metadata = $this->cache->get(
285  $pageKey,
287  );
288 
289  // NOTE: If the value wasn't serialized to JSON when being stored,
290  // we may already have a ParserOutput object here. This used
291  // to be the default behavior before 1.36. We need to retain
292  // support so we can handle cached objects after an update
293  // from an earlier revision.
294  // NOTE: Support for reading string values from the cache must be
295  // deployed a while before starting to write JSON to the cache,
296  // in case we have to revert either change.
297  if ( is_string( $metadata ) && $this->readJson ) {
298  $metadata = $this->restoreFromJson( $metadata, $pageKey, CacheTime::class );
299  }
300 
301  if ( !$metadata instanceof CacheTime ) {
302  $this->incrementStats( $wikiPage, 'miss.unserialize' );
303  return null;
304  }
305 
306  if ( $this->checkExpired( $metadata, $wikiPage, $staleConstraint, 'metadata' ) ) {
307  return null;
308  }
309 
310  if ( $this->checkOutdated( $metadata, $wikiPage, $staleConstraint, 'metadata' ) ) {
311  return null;
312  }
313 
314  $this->logger->debug( 'Parser cache options found', [ 'name' => $this->name ] );
315  return $metadata;
316  }
317 
322  private function makeMetadataKey( WikiPage $wikiPage ): string {
323  return $this->cache->makeKey( $this->name, 'idoptions', $wikiPage->getId() );
324  }
325 
342  public function makeParserOutputKey(
343  WikiPage $wikiPage,
344  ParserOptions $options,
345  array $usedOptions = null
346  ): string {
347  global $wgRequest;
348  $usedOptions = $usedOptions ?? ParserOptions::allCacheVaryingOptions();
349 
350  // idhash seem to mean 'page id' + 'rendering hash' (r3710)
351  $pageid = $wikiPage->getId();
352  // TODO: remove the split T263581
353  $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
354  $hash = $options->optionsHash( $usedOptions, $wikiPage->getTitle() );
355 
356  return $this->cache->makeKey( $this->name, 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
357  }
358 
369  public function get( WikiPage $wikiPage, $popts, $useOutdated = false ) {
370  if ( !$wikiPage->checkTouched() ) {
371  // It's a redirect now
372  $this->incrementStats( $wikiPage, 'miss.redirect' );
373  return false;
374  }
375 
376  $staleConstraint = $useOutdated ? self::USE_OUTDATED : self::USE_CURRENT_ONLY;
377  $parserOutputMetadata = $this->getMetadata( $wikiPage, $staleConstraint );
378  if ( !$parserOutputMetadata ) {
379  return false;
380  }
381 
382  if ( !$popts->isSafeToCache( $parserOutputMetadata->getUsedOptions() ) ) {
383  $this->incrementStats( $wikiPage, 'miss.unsafe' );
384  return false;
385  }
386 
387  $parserOutputKey = $this->makeParserOutputKey(
388  $wikiPage,
389  $popts,
390  $parserOutputMetadata->getUsedOptions()
391  );
392 
393  $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
394  if ( $value === false ) {
395  $this->incrementStats( $wikiPage, "miss.absent" );
396  $this->logger->debug( 'ParserOutput cache miss', [ 'name' => $this->name ] );
397  return false;
398  }
399 
400  // NOTE: If the value wasn't serialized to JSON when being stored,
401  // we may already have a ParserOutput object here. This used
402  // to be the default behavior before 1.36. We need to retain
403  // support so we can handle cached objects after an update
404  // from an earlier revision.
405  // NOTE: Support for reading string values from the cache must be
406  // deployed a while before starting to write JSON to the cache,
407  // in case we have to revert either change.
408  if ( is_string( $value ) && $this->readJson ) {
409  $value = $this->restoreFromJson( $value, $parserOutputKey, ParserOutput::class );
410  }
411 
412  if ( !$value instanceof ParserOutput ) {
413  $this->incrementStats( $wikiPage, 'miss.unserialize' );
414  return false;
415  }
416 
417  if ( $this->checkExpired( $value, $wikiPage, $staleConstraint, 'output' ) ) {
418  return false;
419  }
420 
421  if ( $this->checkOutdated( $value, $wikiPage, $staleConstraint, 'output' ) ) {
422  return false;
423  }
424 
425  if ( $this->hookRunner->onRejectParserCacheValue( $value, $wikiPage, $popts ) === false ) {
426  $this->incrementStats( $wikiPage, 'miss.rejected' );
427  $this->logger->debug( 'key valid, but rejected by RejectParserCacheValue hook handler',
428  [ 'name' => $this->name ] );
429  return false;
430  }
431 
432  $this->logger->debug( 'ParserOutput cache found', [ 'name' => $this->name ] );
433  $this->incrementStats( $wikiPage, 'hit' );
434  return $value;
435  }
436 
444  public function save(
445  ParserOutput $parserOutput,
446  WikiPage $wikiPage,
447  $popts,
448  $cacheTime = null,
449  $revId = null
450  ) {
451  if ( !$parserOutput->hasText() ) {
452  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
453  }
454 
455  $expire = $parserOutput->getCacheExpiry();
456 
457  if ( !$popts->isSafeToCache( $parserOutput->getUsedOptions() ) ) {
458  $this->logger->debug(
459  'Parser options are not safe to cache and has not been saved',
460  [ 'name' => $this->name ]
461  );
462  $this->incrementStats( $wikiPage, 'save.unsafe' );
463  return;
464  }
465 
466  if ( $expire <= 0 ) {
467  $this->logger->debug(
468  'Parser output was marked as uncacheable and has not been saved',
469  [ 'name' => $this->name ]
470  );
471  $this->incrementStats( $wikiPage, 'save.uncacheable' );
472  return;
473  }
474 
475  if ( $this->cache instanceof EmptyBagOStuff ) {
476  return;
477  }
478 
479  $cacheTime = $cacheTime ?: wfTimestampNow();
480  if ( !$revId ) {
481  $revision = $wikiPage->getRevisionRecord();
482  $revId = $revision ? $revision->getId() : null;
483  }
484 
485  $metadata = new CacheTime;
486  $metadata->recordOptions( $parserOutput->getUsedOptions() );
487  $metadata->updateCacheExpiry( $expire );
488 
489  $metadata->setCacheTime( $cacheTime );
490  $parserOutput->setCacheTime( $cacheTime );
491  $metadata->setCacheRevisionId( $revId );
492  $parserOutput->setCacheRevisionId( $revId );
493 
494  $parserOutputKey = $this->makeParserOutputKey(
495  $wikiPage,
496  $popts,
497  $metadata->getUsedOptions()
498  );
499 
500  // Save the timestamp so that we don't have to load the revision row on view
501  $parserOutput->setTimestamp( $wikiPage->getTimestamp() );
502 
503  $msg = "Saved in parser cache with key $parserOutputKey" .
504  " and timestamp $cacheTime" .
505  " and revision id $revId.";
506  if ( $this->writeJson ) {
507  $msg .= " Serialized with JSON.";
508  } else {
509  $msg .= " Serialized with PHP.";
510  }
511  $parserOutput->addCacheMessage( $msg );
512 
513  $pageKey = $this->makeMetadataKey( $wikiPage );
514 
515  if ( $this->writeJson ) {
516  $parserOutputData = $this->encodeAsJson( $parserOutput, $parserOutputKey );
517  $metadataData = $this->encodeAsJson( $metadata, $pageKey );
518  } else {
519  // rely on implicit PHP serialization in the cache
520  $parserOutputData = $parserOutput;
521  $metadataData = $metadata;
522  }
523 
524  if ( !$parserOutputData || !$metadataData ) {
525  $this->logger->warning(
526  'Parser output failed to serialize and was not saved',
527  [ 'name' => $this->name ]
528  );
529  $this->incrementStats( $wikiPage, 'save.nonserializable' );
530  return;
531  }
532 
533  // Save the parser output
534  $this->cache->set(
535  $parserOutputKey,
536  $parserOutputData,
537  $expire,
539  );
540 
541  // ...and its pointer
542  $this->cache->set( $pageKey, $metadataData, $expire );
543 
544  $this->hookRunner->onParserCacheSaveComplete(
545  $this, $parserOutput, $wikiPage->getTitle(), $popts, $revId );
546 
547  $this->logger->debug( 'Saved in parser cache', [
548  'name' => $this->name,
549  'key' => $parserOutputKey,
550  'cache_time' => $cacheTime,
551  'rev_id' => $revId
552  ] );
553  $this->incrementStats( $wikiPage, 'save.success' );
554  }
555 
564  public function getCacheStorage() {
565  return $this->cache;
566  }
567 
577  private function checkExpired(
578  CacheTime $entry,
579  WikiPage $wikiPage,
580  int $staleConstraint,
581  string $cacheTier
582  ): bool {
583  if ( $staleConstraint < self::USE_EXPIRED && $entry->expired( $wikiPage->getTouched() ) ) {
584  $this->incrementStats( $wikiPage, "miss.expired" );
585  $this->logger->debug( "{$cacheTier} key expired", [
586  'name' => $this->name,
587  'touched' => $wikiPage->getTouched(),
588  'epoch' => $this->cacheEpoch,
589  'cache_time' => $entry->getCacheTime()
590  ] );
591  return true;
592  }
593  return false;
594  }
595 
605  private function checkOutdated(
606  CacheTime $entry,
607  WikiPage $wikiPage,
608  int $staleConstraint,
609  string $cacheTier
610  ): bool {
611  $latestRevId = $wikiPage->getLatest();
612  if ( $staleConstraint < self::USE_OUTDATED && $entry->isDifferentRevision( $latestRevId ) ) {
613  $this->incrementStats( $wikiPage, "miss.revid" );
614  $this->logger->debug( "{$cacheTier} key is for an old revision", [
615  'name' => $this->name,
616  'rev_id' => $wikiPage->getLatest(),
617  'cached_rev_id' => $entry->getCacheRevisionId()
618  ] );
619  return true;
620  }
621  return false;
622  }
623 
630  public function setJsonSupport( bool $readJson, bool $writeJson ): void {
631  $this->readJson = $readJson;
632  $this->writeJson = $writeJson;
633  }
634 
641  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
642  try {
644  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
645  return $obj;
646  } catch ( InvalidArgumentException $e ) {
647  $this->logger->error( "Unable to unserialize JSON", [
648  'name' => $this->name,
649  'cache_key' => $key,
650  'message' => $e->getMessage()
651  ] );
652  return null;
653  }
654  }
655 
661  private function encodeAsJson( CacheTime $obj, string $key ) {
662  try {
663  return $this->jsonCodec->serialize( $obj );
664  } catch ( InvalidArgumentException $e ) {
665  $this->logger->error( "Unable to serialize JSON", [
666  'name' => $this->name,
667  'cache_key' => $key,
668  'message' => $e->getMessage(),
669  ] );
670  return null;
671  }
672  }
673 }
ParserOutput\addCacheMessage
addCacheMessage(string $msg)
Adds a comment notice about cache state to the text of the page.
Definition: ParserOutput.php:463
ParserCache\USE_EXPIRED
const USE_EXPIRED
Use expired data if current data is unavailable.
Definition: ParserCache.php:72
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:138
WikiPage\getRevisionRecord
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:849
ParserOutput
Definition: ParserOutput.php:31
CacheTime
Parser cache specific expiry check.
Definition: CacheTime.php:35
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
ParserOutput\setTimestamp
setTimestamp( $timestamp)
Definition: ParserOutput.php:752
ParserCache\checkOutdated
checkOutdated(CacheTime $entry, WikiPage $wikiPage, int $staleConstraint, string $cacheTier)
Check if $entry belongs to the latest revision of $wikiPage given $staleConstraint when fetched from ...
Definition: ParserCache.php:605
ParserCache\deleteOptionsKey
deleteOptionsKey(WikiPage $wikiPage)
Definition: ParserCache.php:173
true
return true
Definition: router.php:90
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:729
BagOStuff\WRITE_ALLOW_SEGMENTS
const WRITE_ALLOW_SEGMENTS
Definition: BagOStuff.php:120
CacheTime\getUsedOptions
getUsedOptions()
Returns the options from its ParserOptions which have been taken into account to produce the output.
Definition: CacheTime.php:210
ParserCache\getKey
getKey(WikiPage $wikiPage, $popts, $useOutdated=self::USE_ANYTHING)
Generates a key for caching the given page considering the given parser options.
Definition: ParserCache.php:240
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:61
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
ParserCache\$cache
BagOStuff $cache
Definition: ParserCache.php:87
CacheTime\recordOptions
recordOptions(array $options)
Tags a list of parser option names for use in the cache key for this parser output.
Definition: CacheTime.php:236
ParserCache\getETag
getETag(WikiPage $wikiPage, $popts)
Provides an E-Tag suitable for the whole page.
Definition: ParserCache.php:192
ParserCache\incrementStats
incrementStats(WikiPage $wikiPage, $metricSuffix)
Definition: ParserCache.php:213
CacheTime\getCacheRevisionId
getCacheRevisionId()
Definition: CacheTime.php:96
ParserCache\setJsonSupport
setJsonSupport(bool $readJson, bool $writeJson)
Definition: ParserCache.php:630
ParserCache\$stats
IBufferingStatsdDataFactory $stats
Definition: ParserCache.php:103
CacheTime\setCacheTime
setCacheTime( $t)
setCacheTime() sets the timestamp expressing when the page has been rendered.
Definition: CacheTime.php:79
ParserCache\getMetadata
getMetadata(WikiPage $wikiPage, int $staleConstraint=self::USE_ANYTHING)
Returns the ParserCache metadata about the given page considering the given options.
Definition: ParserCache.php:279
ParserCache\$writeJson
bool $writeJson
Definition: ParserCache.php:112
ParserCache\makeMetadataKey
makeMetadataKey(WikiPage $wikiPage)
Definition: ParserCache.php:322
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1034
ParserCache\USE_ANYTHING
const USE_ANYTHING
Use expired data and data from different revisions, and if all else fails vary on all variable option...
Definition: ParserCache.php:81
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:317
ParserCache\$name
string $name
The name of this ParserCache.
Definition: ParserCache.php:84
WikiPage\checkTouched
checkTouched()
Loads page_touched and returns a value indicating if it should be used.
Definition: WikiPage.php:721
ParserCache\$hookRunner
HookRunner $hookRunner
Definition: ParserCache.php:97
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1861
WikiPage\getLatest
getLatest()
Get the page_latest field.
Definition: WikiPage.php:751
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:682
ParserCache\$logger
LoggerInterface $logger
Definition: ParserCache.php:106
ParserCache\getCacheStorage
getCacheStorage()
Get the backend BagOStuff instance that powers the parser cache.
Definition: ParserCache.php:564
ParserCache\makeParserOutputKey
makeParserOutputKey(WikiPage $wikiPage, ParserOptions $options, array $usedOptions=null)
Get a key that will be used by the ParserCache to store the content for a given page considering the ...
Definition: ParserCache.php:342
ParserCache\$readJson
bool $readJson
Definition: ParserCache.php:118
ParserCache\restoreFromJson
restoreFromJson(string $jsonData, string $key, string $expectedClass)
Definition: ParserCache.php:641
WikiPage\getId
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:614
CachedBagOStuff
Wrapper around a BagOStuff that caches data in memory.
Definition: CachedBagOStuff.php:37
ParserCache\USE_CURRENT_ONLY
const USE_CURRENT_ONLY
Constants for self::getKey()
Definition: ParserCache.php:69
IBufferingStatsdDataFactory
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Definition: IBufferingStatsdDataFactory.php:13
ParserCache\save
save(ParserOutput $parserOutput, WikiPage $wikiPage, $popts, $cacheTime=null, $revId=null)
Definition: ParserCache.php:444
BagOStuff\READ_VERIFIED
const READ_VERIFIED
Definition: BagOStuff.php:116
ParserOptions\allCacheVaryingOptions
static allCacheVaryingOptions()
Return all option keys that vary the options hash.
Definition: ParserOptions.php:1431
ParserOutput\hasText
hasText()
Returns true if text was passed to the constructor, or set using setText().
Definition: ParserOutput.php:304
CacheTime\setCacheRevisionId
setCacheRevisionId( $id)
Definition: CacheTime.php:104
ParserCache\$cacheEpoch
string $cacheEpoch
Anything cached prior to this is invalidated.
Definition: ParserCache.php:94
ParserCache
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:61
ParserCache\getDirty
getDirty(WikiPage $wikiPage, $popts)
Retrieve the ParserOutput from ParserCache, even if it's outdated.
Definition: ParserCache.php:204
ParserCache\$jsonCodec
JsonCodec $jsonCodec
Definition: ParserCache.php:100
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:576
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:651
MediaWiki\Json\JsonCodec
Definition: JsonCodec.php:36
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:881
ParserCache\checkExpired
checkExpired(CacheTime $entry, WikiPage $wikiPage, int $staleConstraint, string $cacheTier)
Check if $entry expired for $wikiPage given the $staleConstraint when fetching from $cacheTier.
Definition: ParserCache.php:577
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:65
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
ParserCache\encodeAsJson
encodeAsJson(CacheTime $obj, string $key)
Definition: ParserCache.php:661
Parser\ParserCacheMetadata
Read-only interface for metadata about a ParserCache entry.
Definition: ParserCacheMetadata.php:9
ParserCache\USE_OUTDATED
const USE_OUTDATED
Use expired data or data from different revisions if current data is unavailable.
Definition: ParserCache.php:75
ParserCache\__construct
__construct(string $name, BagOStuff $cache, string $cacheEpoch, HookContainer $hookContainer, JsonCodec $jsonCodec, IBufferingStatsdDataFactory $stats, LoggerInterface $logger, $useJson=false)
Setup a cache pathway with a given back-end storage mechanism.
Definition: ParserCache.php:135
ParserOptions\optionsHash
optionsHash( $forOptions, $title=null)
Generate a hash string with the values set on these ParserOptions for the keys given in the array.
Definition: ParserOptions.php:1468
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1131