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  $cache = new CachedBagOStuff( $cache, [
147  // Each ParserCache entry uses 2 keys, one for metadata and one for parser output.
148  // So, cache at most 4 different parser outputs in memory. The number was chosen ad hoc.
149  'maxKeys' => 8
150  ] );
151  }
152  $this->name = $name;
153  $this->cache = $cache;
154  $this->cacheEpoch = $cacheEpoch;
155  $this->hookRunner = new HookRunner( $hookContainer );
156  $this->jsonCodec = $jsonCodec;
157  $this->stats = $stats;
158  $this->logger = $logger;
159  $this->readJson = $useJson;
160  $this->writeJson = $useJson;
161  }
162 
167  public function deleteOptionsKey( WikiPage $wikiPage ) {
168  $this->cache->delete( $this->makeMetadataKey( $wikiPage ) );
169  }
170 
186  public function getETag( WikiPage $wikiPage, $popts ) {
187  wfDeprecated( __METHOD__, '1.36' );
188  return 'W/"' . $this->makeParserOutputKey( $wikiPage, $popts )
189  . "--" . $wikiPage->getTouched() . '"';
190  }
191 
198  public function getDirty( WikiPage $wikiPage, $popts ) {
199  $value = $this->get( $wikiPage, $popts, true );
200  return is_object( $value ) ? $value : false;
201  }
202 
207  private function incrementStats( WikiPage $wikiPage, $metricSuffix ) {
208  $contentModel = str_replace( '.', '_', $wikiPage->getContentModel() );
209  $metricSuffix = str_replace( '.', '_', $metricSuffix );
210  $this->stats->increment( "{$this->name}.{$contentModel}.{$metricSuffix}" );
211  }
212 
234  public function getKey( WikiPage $wikiPage, $popts, $useOutdated = self::USE_ANYTHING ) {
235  wfDeprecated( __METHOD__, '1.36' );
236  if ( is_bool( $useOutdated ) ) {
237  $useOutdated = $useOutdated ? self::USE_ANYTHING : self::USE_CURRENT_ONLY;
238  }
239 
240  if ( $popts instanceof User ) {
241  $this->logger->warning(
242  "Use of outdated prototype ParserCache::getKey( &\$wikiPage, &\$user )\n"
243  );
244  $popts = ParserOptions::newFromUser( $popts );
245  }
246 
247  $metadata = $this->getMetadata( $wikiPage, $useOutdated );
248  if ( !$metadata ) {
249  if ( $useOutdated < self::USE_ANYTHING ) {
250  return false;
251  }
252  $usedOptions = ParserOptions::allCacheVaryingOptions();
253  } else {
254  $usedOptions = $metadata->getUsedOptions();
255  }
256 
257  return $this->makeParserOutputKey( $wikiPage, $popts, $usedOptions );
258  }
259 
273  public function getMetadata(
274  WikiPage $wikiPage,
275  int $staleConstraint = self::USE_ANYTHING
276  ): ?ParserCacheMetadata {
277  $pageKey = $this->makeMetadataKey( $wikiPage );
278  $metadata = $this->cache->get(
279  $pageKey,
281  );
282 
283  // NOTE: If the value wasn't serialized to JSON when being stored,
284  // we may already have a ParserOutput object here. This used
285  // to be the default behavior before 1.36. We need to retain
286  // support so we can handle cached objects after an update
287  // from an earlier revision.
288  // NOTE: Support for reading string values from the cache must be
289  // deployed a while before starting to write JSON to the cache,
290  // in case we have to revert either change.
291  if ( is_string( $metadata ) && $this->readJson ) {
292  $metadata = $this->restoreFromJson( $metadata, $pageKey, CacheTime::class );
293  }
294 
295  if ( !$metadata instanceof CacheTime ) {
296  $this->incrementStats( $wikiPage, 'miss.unserialize' );
297  return null;
298  }
299 
300  if ( $this->checkExpired( $metadata, $wikiPage, $staleConstraint, 'metadata' ) ) {
301  return null;
302  }
303 
304  if ( $this->checkOutdated( $metadata, $wikiPage, $staleConstraint, 'metadata' ) ) {
305  return null;
306  }
307 
308  $this->logger->debug( 'Parser cache options found', [ 'name' => $this->name ] );
309  return $metadata;
310  }
311 
316  private function makeMetadataKey( WikiPage $wikiPage ): string {
317  return $this->cache->makeKey( $this->name, 'idoptions', $wikiPage->getId() );
318  }
319 
336  public function makeParserOutputKey(
337  WikiPage $wikiPage,
338  ParserOptions $options,
339  array $usedOptions = null
340  ): string {
341  global $wgRequest;
342  $usedOptions = $usedOptions ?? ParserOptions::allCacheVaryingOptions();
343 
344  // idhash seem to mean 'page id' + 'rendering hash' (r3710)
345  $pageid = $wikiPage->getId();
346  // TODO: remove the split T263581
347  $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
348  $hash = $options->optionsHash( $usedOptions, $wikiPage->getTitle() );
349 
350  return $this->cache->makeKey( $this->name, 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
351  }
352 
363  public function get( WikiPage $wikiPage, $popts, $useOutdated = false ) {
364  if ( !$wikiPage->checkTouched() ) {
365  // It's a redirect now
366  $this->incrementStats( $wikiPage, 'miss.redirect' );
367  return false;
368  }
369 
370  $staleConstraint = $useOutdated ? self::USE_OUTDATED : self::USE_CURRENT_ONLY;
371  $parserOutputMetadata = $this->getMetadata( $wikiPage, $staleConstraint );
372  if ( !$parserOutputMetadata ) {
373  return false;
374  }
375 
376  if ( !$popts->isSafeToCache( $parserOutputMetadata->getUsedOptions() ) ) {
377  $this->incrementStats( $wikiPage, 'miss.unsafe' );
378  return false;
379  }
380 
381  $parserOutputKey = $this->makeParserOutputKey(
382  $wikiPage,
383  $popts,
384  $parserOutputMetadata->getUsedOptions()
385  );
386 
387  $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
388  if ( $value === false ) {
389  $this->incrementStats( $wikiPage, "miss.absent" );
390  $this->logger->debug( 'ParserOutput cache miss', [ 'name' => $this->name ] );
391  return false;
392  }
393 
394  // NOTE: If the value wasn't serialized to JSON when being stored,
395  // we may already have a ParserOutput object here. This used
396  // to be the default behavior before 1.36. We need to retain
397  // support so we can handle cached objects after an update
398  // from an earlier revision.
399  // NOTE: Support for reading string values from the cache must be
400  // deployed a while before starting to write JSON to the cache,
401  // in case we have to revert either change.
402  if ( is_string( $value ) && $this->readJson ) {
403  $value = $this->restoreFromJson( $value, $parserOutputKey, ParserOutput::class );
404  }
405 
406  if ( !$value instanceof ParserOutput ) {
407  $this->incrementStats( $wikiPage, 'miss.unserialize' );
408  return false;
409  }
410 
411  if ( $this->checkExpired( $value, $wikiPage, $staleConstraint, 'output' ) ) {
412  return false;
413  }
414 
415  if ( $this->checkOutdated( $value, $wikiPage, $staleConstraint, 'output' ) ) {
416  return false;
417  }
418 
419  if ( $this->hookRunner->onRejectParserCacheValue( $value, $wikiPage, $popts ) === false ) {
420  $this->incrementStats( $wikiPage, 'miss.rejected' );
421  $this->logger->debug( 'key valid, but rejected by RejectParserCacheValue hook handler',
422  [ 'name' => $this->name ] );
423  return false;
424  }
425 
426  $this->logger->debug( 'ParserOutput cache found', [ 'name' => $this->name ] );
427  $this->incrementStats( $wikiPage, 'hit' );
428  return $value;
429  }
430 
438  public function save(
439  ParserOutput $parserOutput,
440  WikiPage $wikiPage,
441  $popts,
442  $cacheTime = null,
443  $revId = null
444  ) {
445  if ( !$parserOutput->hasText() ) {
446  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
447  }
448 
449  $expire = $parserOutput->getCacheExpiry();
450 
451  if ( !$popts->isSafeToCache( $parserOutput->getUsedOptions() ) ) {
452  $this->logger->debug(
453  'Parser options are not safe to cache and has not been saved',
454  [ 'name' => $this->name ]
455  );
456  $this->incrementStats( $wikiPage, 'save.unsafe' );
457  return;
458  }
459 
460  if ( $expire <= 0 ) {
461  $this->logger->debug(
462  'Parser output was marked as uncacheable and has not been saved',
463  [ 'name' => $this->name ]
464  );
465  $this->incrementStats( $wikiPage, 'save.uncacheable' );
466  return;
467  }
468 
469  if ( $this->cache instanceof EmptyBagOStuff ) {
470  return;
471  }
472 
473  $cacheTime = $cacheTime ?: wfTimestampNow();
474  if ( !$revId ) {
475  $revision = $wikiPage->getRevisionRecord();
476  $revId = $revision ? $revision->getId() : null;
477  }
478 
479  $metadata = new CacheTime;
480  $metadata->recordOptions( $parserOutput->getUsedOptions() );
481  $metadata->updateCacheExpiry( $expire );
482 
483  $metadata->setCacheTime( $cacheTime );
484  $parserOutput->setCacheTime( $cacheTime );
485  $metadata->setCacheRevisionId( $revId );
486  $parserOutput->setCacheRevisionId( $revId );
487 
488  $parserOutputKey = $this->makeParserOutputKey(
489  $wikiPage,
490  $popts,
491  $metadata->getUsedOptions()
492  );
493 
494  // Save the timestamp so that we don't have to load the revision row on view
495  $parserOutput->setTimestamp( $wikiPage->getTimestamp() );
496 
497  $msg = "Saved in parser cache with key $parserOutputKey" .
498  " and timestamp $cacheTime" .
499  " and revision id $revId.";
500  if ( $this->writeJson ) {
501  $msg .= " Serialized with JSON.";
502  } else {
503  $msg .= " Serialized with PHP.";
504  }
505  $parserOutput->addCacheMessage( $msg );
506 
507  $pageKey = $this->makeMetadataKey( $wikiPage );
508 
509  if ( $this->writeJson ) {
510  $parserOutputData = $this->encodeAsJson( $parserOutput, $parserOutputKey );
511  $metadataData = $this->encodeAsJson( $metadata, $pageKey );
512  } else {
513  // rely on implicit PHP serialization in the cache
514  $parserOutputData = $parserOutput;
515  $metadataData = $metadata;
516  }
517 
518  if ( !$parserOutputData || !$metadataData ) {
519  $this->logger->warning(
520  'Parser output failed to serialize and was not saved',
521  [ 'name' => $this->name ]
522  );
523  $this->incrementStats( $wikiPage, 'save.nonserializable' );
524  return;
525  }
526 
527  // Save the parser output
528  $this->cache->set(
529  $parserOutputKey,
530  $parserOutputData,
531  $expire,
533  );
534 
535  // ...and its pointer
536  $this->cache->set( $pageKey, $metadataData, $expire );
537 
538  $this->hookRunner->onParserCacheSaveComplete(
539  $this, $parserOutput, $wikiPage->getTitle(), $popts, $revId );
540 
541  $this->logger->debug( 'Saved in parser cache', [
542  'name' => $this->name,
543  'key' => $parserOutputKey,
544  'cache_time' => $cacheTime,
545  'rev_id' => $revId
546  ] );
547  $this->incrementStats( $wikiPage, 'save.success' );
548  }
549 
558  public function getCacheStorage() {
559  return $this->cache;
560  }
561 
571  private function checkExpired(
572  CacheTime $entry,
573  WikiPage $wikiPage,
574  int $staleConstraint,
575  string $cacheTier
576  ): bool {
577  if ( $staleConstraint < self::USE_EXPIRED && $entry->expired( $wikiPage->getTouched() ) ) {
578  $this->incrementStats( $wikiPage, "miss.expired" );
579  $this->logger->debug( "{$cacheTier} key expired", [
580  'name' => $this->name,
581  'touched' => $wikiPage->getTouched(),
582  'epoch' => $this->cacheEpoch,
583  'cache_time' => $entry->getCacheTime()
584  ] );
585  return true;
586  }
587  return false;
588  }
589 
599  private function checkOutdated(
600  CacheTime $entry,
601  WikiPage $wikiPage,
602  int $staleConstraint,
603  string $cacheTier
604  ): bool {
605  $latestRevId = $wikiPage->getLatest();
606  if ( $staleConstraint < self::USE_OUTDATED && $entry->isDifferentRevision( $latestRevId ) ) {
607  $this->incrementStats( $wikiPage, "miss.revid" );
608  $this->logger->debug( "{$cacheTier} key is for an old revision", [
609  'name' => $this->name,
610  'rev_id' => $wikiPage->getLatest(),
611  'cached_rev_id' => $entry->getCacheRevisionId()
612  ] );
613  return true;
614  }
615  return false;
616  }
617 
624  public function setJsonSupport( bool $readJson, bool $writeJson ): void {
625  $this->readJson = $readJson;
626  $this->writeJson = $writeJson;
627  }
628 
635  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
636  try {
638  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
639  return $obj;
640  } catch ( InvalidArgumentException $e ) {
641  $this->logger->error( "Unable to unserialize JSON", [
642  'name' => $this->name,
643  'cache_key' => $key,
644  'message' => $e->getMessage()
645  ] );
646  return null;
647  }
648  }
649 
655  private function encodeAsJson( CacheTime $obj, string $key ) {
656  try {
657  return $this->jsonCodec->serialize( $obj );
658  } catch ( InvalidArgumentException $e ) {
659  $this->logger->error( "Unable to serialize JSON", [
660  'name' => $this->name,
661  'cache_key' => $key,
662  'message' => $e->getMessage(),
663  ] );
664  return null;
665  }
666  }
667 }
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:829
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:599
ParserCache\deleteOptionsKey
deleteOptionsKey(WikiPage $wikiPage)
Definition: ParserCache.php:167
true
return true
Definition: router.php:90
WikiPage\getTouched
getTouched()
Get the page_touched field.
Definition: WikiPage.php:709
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:234
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:186
ParserCache\incrementStats
incrementStats(WikiPage $wikiPage, $metricSuffix)
Definition: ParserCache.php:207
CacheTime\getCacheRevisionId
getCacheRevisionId()
Definition: CacheTime.php:96
ParserCache\setJsonSupport
setJsonSupport(bool $readJson, bool $writeJson)
Definition: ParserCache.php:624
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:273
ParserCache\$writeJson
bool $writeJson
Definition: ParserCache.php:112
ParserCache\makeMetadataKey
makeMetadataKey(WikiPage $wikiPage)
Definition: ParserCache.php:316
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:308
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:698
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:731
WikiPage\getContentModel
getContentModel()
Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
Definition: WikiPage.php:659
ParserCache\$logger
LoggerInterface $logger
Definition: ParserCache.php:106
ParserCache\getCacheStorage
getCacheStorage()
Get the backend BagOStuff instance that powers the parser cache.
Definition: ParserCache.php:558
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:336
ParserCache\$readJson
bool $readJson
Definition: ParserCache.php:118
ParserCache\restoreFromJson
restoreFromJson(string $jsonData, string $key, string $expectedClass)
Definition: ParserCache.php:635
WikiPage\getId
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:604
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:438
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:1428
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:198
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:575
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:651
MediaWiki\Json\JsonCodec
Definition: JsonCodec.php:36
WikiPage\getTimestamp
getTimestamp()
Definition: WikiPage.php:861
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:571
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:655
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:1465
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1131