MediaWiki  master
ParserCache.php
Go to the documentation of this file.
1 <?php
30 use Psr\Log\LoggerInterface;
31 
63 class ParserCache {
71  public const USE_CURRENT_ONLY = 0;
72 
74  public const USE_EXPIRED = 1;
75 
77  public const USE_OUTDATED = 2;
78 
83  private const USE_ANYTHING = 3;
84 
86  private $name;
87 
89  private $cache;
90 
96  private $cacheEpoch;
97 
99  private $hookRunner;
100 
102  private $jsonCodec;
103 
105  private $stats;
106 
108  private $logger;
109 
111  private $titleFactory;
112 
115 
123 
128  private $writeJson = false;
129 
134  private $readJson = false;
135 
153  public function __construct(
154  string $name,
156  string $cacheEpoch,
157  HookContainer $hookContainer,
160  LoggerInterface $logger,
163  $useJson = false
164  ) {
165  $this->name = $name;
166  $this->cache = $cache;
167  $this->cacheEpoch = $cacheEpoch;
168  $this->hookRunner = new HookRunner( $hookContainer );
169  $this->jsonCodec = $jsonCodec;
170  $this->stats = $stats;
171  $this->logger = $logger;
172  $this->titleFactory = $titleFactory;
173  $this->wikiPageFactory = $wikiPageFactory;
174  $this->readJson = $useJson;
175  $this->writeJson = $useJson;
176  $this->metadataProcCache = new HashBagOStuff( [ 'maxKeys' => 2 ] );
177  }
178 
183  public function deleteOptionsKey( PageRecord $page ) {
184  $page->assertWiki( PageRecord::LOCAL );
185  $key = $this->makeMetadataKey( $page );
186  $this->metadataProcCache->delete( $key );
187  $this->cache->delete( $key );
188  }
189 
196  public function getDirty( PageRecord $page, $popts ) {
197  $page->assertWiki( PageRecord::LOCAL );
198  $value = $this->get( $page, $popts, true );
199  return is_object( $value ) ? $value : false;
200  }
201 
206  private function incrementStats( PageRecord $page, $metricSuffix ) {
207  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
208  $contentModel = str_replace( '.', '_', $wikiPage->getContentModel() );
209  $metricSuffix = str_replace( '.', '_', $metricSuffix );
210  $this->stats->increment( "{$this->name}.{$contentModel}.{$metricSuffix}" );
211  }
212 
226  public function getMetadata(
227  PageRecord $page,
228  int $staleConstraint = self::USE_ANYTHING
229  ): ?ParserCacheMetadata {
230  $page->assertWiki( PageRecord::LOCAL );
231 
232  $pageKey = $this->makeMetadataKey( $page );
233  $metadata = $this->metadataProcCache->get( $pageKey );
234  if ( !$metadata ) {
235  $metadata = $this->cache->get(
236  $pageKey,
238  );
239  }
240 
241  if ( $metadata === false ) {
242  $this->incrementStats( $page, "miss.absent.metadata" );
243  $this->logger->debug( 'ParserOutput metadata cache miss', [ 'name' => $this->name ] );
244  return null;
245  }
246 
247  // NOTE: If the value wasn't serialized to JSON when being stored,
248  // we may already have a ParserOutput object here. This used
249  // to be the default behavior before 1.36. We need to retain
250  // support so we can handle cached objects after an update
251  // from an earlier revision.
252  // NOTE: Support for reading string values from the cache must be
253  // deployed a while before starting to write JSON to the cache,
254  // in case we have to revert either change.
255  if ( is_string( $metadata ) && $this->readJson ) {
256  $metadata = $this->restoreFromJson( $metadata, $pageKey, CacheTime::class );
257  }
258 
259  if ( !$metadata instanceof CacheTime ) {
260  $this->incrementStats( $page, 'miss.unserialize' );
261  return null;
262  }
263 
264  if ( $this->checkExpired( $metadata, $page, $staleConstraint, 'metadata' ) ) {
265  return null;
266  }
267 
268  if ( $this->checkOutdated( $metadata, $page, $staleConstraint, 'metadata' ) ) {
269  return null;
270  }
271 
272  $this->logger->debug( 'Parser cache options found', [ 'name' => $this->name ] );
273  return $metadata;
274  }
275 
280  private function makeMetadataKey( PageRecord $page ): string {
281  return $this->cache->makeKey( $this->name, 'idoptions', $page->getId( PageRecord::LOCAL ) );
282  }
283 
300  public function makeParserOutputKey(
301  PageRecord $page,
302  ParserOptions $options,
303  array $usedOptions = null
304  ): string {
305  $usedOptions = $usedOptions ?? ParserOptions::allCacheVaryingOptions();
306  // idhash seem to mean 'page id' + 'rendering hash' (r3710)
307  $pageid = $page->getId( PageRecord::LOCAL );
308  $title = $this->titleFactory->castFromPageIdentity( $page );
309  $hash = $options->optionsHash( $usedOptions, $title );
310  // Before T263581 ParserCache was split between normal page views
311  // and action=parse. -0 is left in the key to avoid invalidating the entire
312  // cache when removing the cache split.
313  return $this->cache->makeKey( $this->name, 'idhash', "{$pageid}-0!{$hash}" );
314  }
315 
326  public function get( PageRecord $page, $popts, $useOutdated = false ) {
327  $page->assertWiki( PageRecord::LOCAL );
328 
329  if ( !$page->exists() ) {
330  $this->incrementStats( $page, 'miss.nonexistent' );
331  return false;
332  }
333 
334  if ( $page->isRedirect() ) {
335  // It's a redirect now
336  $this->incrementStats( $page, 'miss.redirect' );
337  return false;
338  }
339 
340  $staleConstraint = $useOutdated ? self::USE_OUTDATED : self::USE_CURRENT_ONLY;
341  $parserOutputMetadata = $this->getMetadata( $page, $staleConstraint );
342  if ( !$parserOutputMetadata ) {
343  return false;
344  }
345 
346  if ( !$popts->isSafeToCache( $parserOutputMetadata->getUsedOptions() ) ) {
347  $this->incrementStats( $page, 'miss.unsafe' );
348  return false;
349  }
350 
351  $parserOutputKey = $this->makeParserOutputKey(
352  $page,
353  $popts,
354  $parserOutputMetadata->getUsedOptions()
355  );
356 
357  $value = $this->cache->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
358  if ( $value === false ) {
359  $this->incrementStats( $page, "miss.absent" );
360  $this->logger->debug( 'ParserOutput cache miss', [ 'name' => $this->name ] );
361  return false;
362  }
363 
364  // NOTE: If the value wasn't serialized to JSON when being stored,
365  // we may already have a ParserOutput object here. This used
366  // to be the default behavior before 1.36. We need to retain
367  // support so we can handle cached objects after an update
368  // from an earlier revision.
369  // NOTE: Support for reading string values from the cache must be
370  // deployed a while before starting to write JSON to the cache,
371  // in case we have to revert either change.
372  if ( is_string( $value ) && $this->readJson ) {
373  $value = $this->restoreFromJson( $value, $parserOutputKey, ParserOutput::class );
374  }
375 
376  if ( !$value instanceof ParserOutput ) {
377  $this->incrementStats( $page, 'miss.unserialize' );
378  return false;
379  }
380 
381  if ( $this->checkExpired( $value, $page, $staleConstraint, 'output' ) ) {
382  return false;
383  }
384 
385  if ( $this->checkOutdated( $value, $page, $staleConstraint, 'output' ) ) {
386  return false;
387  }
388 
389  $wikiPage = $this->wikiPageFactory->newFromTitle( $page );
390  if ( $this->hookRunner->onRejectParserCacheValue( $value, $wikiPage, $popts ) === false ) {
391  $this->incrementStats( $page, 'miss.rejected' );
392  $this->logger->debug( 'key valid, but rejected by RejectParserCacheValue hook handler',
393  [ 'name' => $this->name ] );
394  return false;
395  }
396 
397  $this->logger->debug( 'ParserOutput cache found', [ 'name' => $this->name ] );
398  $this->incrementStats( $page, 'hit' );
399  return $value;
400  }
401 
409  public function save(
410  ParserOutput $parserOutput,
411  PageRecord $page,
412  $popts,
413  $cacheTime = null,
414  $revId = null
415  ) {
416  $page->assertWiki( PageRecord::LOCAL );
417 
418  if ( !$parserOutput->hasText() ) {
419  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
420  }
421 
422  $expire = $parserOutput->getCacheExpiry();
423 
424  if ( !$popts->isSafeToCache( $parserOutput->getUsedOptions() ) ) {
425  $this->logger->debug(
426  'Parser options are not safe to cache and has not been saved',
427  [ 'name' => $this->name ]
428  );
429  $this->incrementStats( $page, 'save.unsafe' );
430  return;
431  }
432 
433  if ( $expire <= 0 ) {
434  $this->logger->debug(
435  'Parser output was marked as uncacheable and has not been saved',
436  [ 'name' => $this->name ]
437  );
438  $this->incrementStats( $page, 'save.uncacheable' );
439  return;
440  }
441 
442  if ( $this->cache instanceof EmptyBagOStuff ) {
443  return;
444  }
445 
446  $cacheTime = $cacheTime ?: wfTimestampNow();
447  $revId = $revId ?: $page->getLatest( PageRecord::LOCAL );
448 
449  $metadata = new CacheTime;
450  $metadata->recordOptions( $parserOutput->getUsedOptions() );
451  $metadata->updateCacheExpiry( $expire );
452 
453  $metadata->setCacheTime( $cacheTime );
454  $parserOutput->setCacheTime( $cacheTime );
455  $metadata->setCacheRevisionId( $revId );
456  $parserOutput->setCacheRevisionId( $revId );
457 
458  $parserOutputKey = $this->makeParserOutputKey(
459  $page,
460  $popts,
461  $metadata->getUsedOptions()
462  );
463 
464  $msg = "Saved in parser cache with key $parserOutputKey" .
465  " and timestamp $cacheTime" .
466  " and revision id $revId.";
467  if ( $this->writeJson ) {
468  $msg .= " Serialized with JSON.";
469  } else {
470  $msg .= " Serialized with PHP.";
471  }
472  $parserOutput->addCacheMessage( $msg );
473 
474  $pageKey = $this->makeMetadataKey( $page );
475 
476  if ( $this->writeJson ) {
477  $parserOutputData = $this->encodeAsJson( $parserOutput, $parserOutputKey );
478  $metadataData = $this->encodeAsJson( $metadata, $pageKey );
479  } else {
480  // rely on implicit PHP serialization in the cache
481  $parserOutputData = $parserOutput;
482  $metadataData = $metadata;
483  }
484 
485  if ( !$parserOutputData || !$metadataData ) {
486  $this->logger->warning(
487  'Parser output failed to serialize and was not saved',
488  [ 'name' => $this->name ]
489  );
490  $this->incrementStats( $page, 'save.nonserializable' );
491  return;
492  }
493 
494  // Save the parser output
495  $this->cache->set(
496  $parserOutputKey,
497  $parserOutputData,
498  $expire,
500  );
501 
502  // ...and its pointer to the local cache.
503  $this->metadataProcCache->set( $pageKey, $metadataData, $expire );
504  // ...and to the global cache.
505  $this->cache->set( $pageKey, $metadataData, $expire );
506 
507  $title = $this->titleFactory->castFromPageIdentity( $page );
508  $this->hookRunner->onParserCacheSaveComplete( $this, $parserOutput, $title, $popts, $revId );
509 
510  $this->logger->debug( 'Saved in parser cache', [
511  'name' => $this->name,
512  'key' => $parserOutputKey,
513  'cache_time' => $cacheTime,
514  'rev_id' => $revId
515  ] );
516  $this->incrementStats( $page, 'save.success' );
517  }
518 
527  public function getCacheStorage() {
528  return $this->cache;
529  }
530 
540  private function checkExpired(
541  CacheTime $entry,
542  PageRecord $page,
543  int $staleConstraint,
544  string $cacheTier
545  ): bool {
546  if ( $staleConstraint < self::USE_EXPIRED && $entry->expired( $page->getTouched() ) ) {
547  $this->incrementStats( $page, "miss.expired" );
548  $this->logger->debug( "{$cacheTier} key expired", [
549  'name' => $this->name,
550  'touched' => $page->getTouched(),
551  'epoch' => $this->cacheEpoch,
552  'cache_time' => $entry->getCacheTime()
553  ] );
554  return true;
555  }
556  return false;
557  }
558 
568  private function checkOutdated(
569  CacheTime $entry,
570  PageRecord $page,
571  int $staleConstraint,
572  string $cacheTier
573  ): bool {
574  $latestRevId = $page->getLatest( PageRecord::LOCAL );
575  if ( $staleConstraint < self::USE_OUTDATED && $entry->isDifferentRevision( $latestRevId ) ) {
576  $this->incrementStats( $page, "miss.revid" );
577  $this->logger->debug( "{$cacheTier} key is for an old revision", [
578  'name' => $this->name,
579  'rev_id' => $latestRevId,
580  'cached_rev_id' => $entry->getCacheRevisionId()
581  ] );
582  return true;
583  }
584  return false;
585  }
586 
593  public function setJsonSupport( bool $readJson, bool $writeJson ): void {
594  $this->readJson = $readJson;
595  $this->writeJson = $writeJson;
596  }
597 
604  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
605  try {
607  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
608  return $obj;
609  } catch ( InvalidArgumentException $e ) {
610  $this->logger->error( "Unable to unserialize JSON", [
611  'name' => $this->name,
612  'cache_key' => $key,
613  'message' => $e->getMessage()
614  ] );
615  return null;
616  }
617  }
618 
624  private function encodeAsJson( CacheTime $obj, string $key ) {
625  try {
626  return $this->jsonCodec->serialize( $obj );
627  } catch ( InvalidArgumentException $e ) {
628  $this->logger->error( "Unable to serialize JSON", [
629  'name' => $this->name,
630  'cache_key' => $key,
631  'message' => $e->getMessage(),
632  ] );
633  return null;
634  }
635  }
636 }
ParserOutput\addCacheMessage
addCacheMessage(string $msg)
Adds a comment notice about cache state to the text of the page.
Definition: ParserOutput.php:485
ParserCache\checkExpired
checkExpired(CacheTime $entry, PageRecord $page, int $staleConstraint, string $cacheTier)
Check if $entry expired for $page given the $staleConstraint when fetching from $cacheTier.
Definition: ParserCache.php:540
ParserCache\USE_EXPIRED
const USE_EXPIRED
Use expired data if current data is unavailable.
Definition: ParserCache.php:74
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
CacheTime\getCacheExpiry
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:142
Page\PageRecord
Data record representing a page that is (or used to be, or could be) an editable page on a wiki.
Definition: PageRecord.php:25
ParserOutput
Definition: ParserOutput.php:36
CacheTime
Parser cache specific expiry check.
Definition: CacheTime.php:35
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:32
Page\PageRecord\getLatest
getLatest( $wikiId=self::LOCAL)
The ID of the page's latest revision.
ParserCache\$metadataProcCache
BagOStuff $metadataProcCache
small in-process cache to store metadata.
Definition: ParserCache.php:122
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
ParserCache\incrementStats
incrementStats(PageRecord $page, $metricSuffix)
Definition: ParserCache.php:206
ParserCache\$titleFactory
TitleFactory $titleFactory
Definition: ParserCache.php:111
ParserCache\__construct
__construct(string $name, BagOStuff $cache, string $cacheEpoch, HookContainer $hookContainer, JsonCodec $jsonCodec, IBufferingStatsdDataFactory $stats, LoggerInterface $logger, TitleFactory $titleFactory, WikiPageFactory $wikiPageFactory, $useJson=false)
Setup a cache pathway with a given back-end storage mechanism.
Definition: ParserCache.php:153
true
return true
Definition: router.php:90
ParserCache\makeParserOutputKey
makeParserOutputKey(PageRecord $page, 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:300
BagOStuff\WRITE_ALLOW_SEGMENTS
const WRITE_ALLOW_SEGMENTS
Definition: BagOStuff.php:114
Page\PageRecord\isRedirect
isRedirect()
True if the page is a redirect.
CacheTime\getUsedOptions
getUsedOptions()
Returns the options from its ParserOptions which have been taken into account to produce the output.
Definition: CacheTime.php:214
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
ParserCache\$cache
BagOStuff $cache
Definition: ParserCache.php:89
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:240
ParserCache\deleteOptionsKey
deleteOptionsKey(PageRecord $page)
Definition: ParserCache.php:183
CacheTime\getCacheRevisionId
getCacheRevisionId()
Definition: CacheTime.php:96
ParserCache\setJsonSupport
setJsonSupport(bool $readJson, bool $writeJson)
Definition: ParserCache.php:593
ParserCache\makeMetadataKey
makeMetadataKey(PageRecord $page)
Definition: ParserCache.php:280
ParserCache\$stats
IBufferingStatsdDataFactory $stats
Definition: ParserCache.php:105
CacheTime\setCacheTime
setCacheTime( $t)
setCacheTime() sets the timestamp expressing when the page has been rendered.
Definition: CacheTime.php:79
ParserCache\$writeJson
bool $writeJson
Definition: ParserCache.php:128
Page\ProperPageIdentity\getId
getId( $wikiId=self::LOCAL)
Returns the page ID.
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:83
ParserCache\$name
string $name
The name of this ParserCache.
Definition: ParserCache.php:86
ParserCache\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: ParserCache.php:114
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
ParserCache\$hookRunner
HookRunner $hookRunner
Definition: ParserCache.php:99
Page\PageRecord\getTouched
getTouched()
Timestamp at which the page was last flagged for rerendering.
$title
$title
Definition: testCompression.php:38
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1694
ParserCache\save
save(ParserOutput $parserOutput, PageRecord $page, $popts, $cacheTime=null, $revId=null)
Definition: ParserCache.php:409
ParserCache\$logger
LoggerInterface $logger
Definition: ParserCache.php:108
ParserCache\getCacheStorage
getCacheStorage()
Get the backend BagOStuff instance that powers the parser cache.
Definition: ParserCache.php:527
ParserCache\$readJson
bool $readJson
Definition: ParserCache.php:134
ParserCache\restoreFromJson
restoreFromJson(string $jsonData, string $key, string $expectedClass)
Definition: ParserCache.php:604
MediaWiki\DAO\WikiAwareEntity\assertWiki
assertWiki( $wikiId)
Throws if $wikiId is different from the return value of getWikiId().
ParserCache\getDirty
getDirty(PageRecord $page, $popts)
Retrieve the ParserOutput from ParserCache, even if it's outdated.
Definition: ParserCache.php:196
ParserCache\USE_CURRENT_ONLY
const USE_CURRENT_ONLY
Constants for self::getKey()
Definition: ParserCache.php:71
IBufferingStatsdDataFactory
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Definition: IBufferingStatsdDataFactory.php:13
ParserCache\checkOutdated
checkOutdated(CacheTime $entry, PageRecord $page, int $staleConstraint, string $cacheTier)
Check if $entry belongs to the latest revision of $page given $staleConstraint when fetched from $cac...
Definition: ParserCache.php:568
BagOStuff\READ_VERIFIED
const READ_VERIFIED
Definition: BagOStuff.php:110
ParserOptions\allCacheVaryingOptions
static allCacheVaryingOptions()
Return all option keys that vary the options hash.
Definition: ParserOptions.php:1335
ParserOutput\hasText
hasText()
Returns true if text was passed to the constructor, or set using setText().
Definition: ParserOutput.php:309
CacheTime\setCacheRevisionId
setCacheRevisionId( $id)
Definition: CacheTime.php:104
TitleFactory
Creates Title objects.
Definition: TitleFactory.php:35
ParserCache\$cacheEpoch
string $cacheEpoch
Anything cached prior to this is invalidated.
Definition: ParserCache.php:96
Page\PageIdentity\exists
exists()
Checks if the page currently exists.
ParserCache
Cache for ParserOutput objects corresponding to the latest page revisions.
Definition: ParserCache.php:63
ParserCache\$jsonCodec
JsonCodec $jsonCodec
Definition: ParserCache.php:102
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:557
MediaWiki\Json\JsonCodec
Definition: JsonCodec.php:36
CacheTime\getCacheTime
getCacheTime()
Definition: CacheTime.php:65
ParserCache\encodeAsJson
encodeAsJson(CacheTime $obj, string $key)
Definition: ParserCache.php:624
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:77
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:1372
ParserCache\getMetadata
getMetadata(PageRecord $page, int $staleConstraint=self::USE_ANYTHING)
Returns the ParserCache metadata about the given page considering the given options.
Definition: ParserCache.php:226