MediaWiki  master
RevisionOutputCache.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Parser;
25 
26 use CacheTime;
28 use InvalidArgumentException;
32 use ParserOptions;
33 use ParserOutput;
34 use Psr\Log\LoggerInterface;
35 use WANObjectCache;
36 
45 
47  private $name;
48 
50  private $cache;
51 
57  private $cacheEpoch;
58 
64  private $cacheExpiry;
65 
67  private $jsonCodec;
68 
70  private $stats;
71 
73  private $logger;
74 
84  public function __construct(
85  string $name,
86  WANObjectCache $cache,
87  int $cacheExpiry,
88  string $cacheEpoch,
89  JsonCodec $jsonCodec,
91  LoggerInterface $logger
92  ) {
93  $this->name = $name;
94  $this->cache = $cache;
95  $this->cacheExpiry = $cacheExpiry;
96  $this->cacheEpoch = $cacheEpoch;
97  $this->jsonCodec = $jsonCodec;
98  $this->stats = $stats;
99  $this->logger = $logger;
100  }
101 
105  private function incrementStats( string $metricSuffix ) {
106  $metricSuffix = str_replace( '.', '_', $metricSuffix );
107  $this->stats->increment( "RevisionOutputCache.{$this->name}.{$metricSuffix}" );
108  }
109 
128  public function makeParserOutputKey(
129  RevisionRecord $revision,
130  ParserOptions $options,
131  array $usedOptions = null
132  ): string {
133  $usedOptions = ParserOptions::allCacheVaryingOptions();
134 
135  $revId = $revision->getId();
136  if ( !$revId ) {
137  // If RevId is null, this would probably be unsafe to use as a cache key.
138  throw new InvalidArgumentException( "Revision must have an id number" );
139  }
140  $hash = $options->optionsHash( $usedOptions );
141  return $this->cache->makeKey( $this->name, $revId, $hash );
142  }
143 
162  RevisionRecord $revision,
163  ParserOptions $options,
164  array $usedOptions = null
165  ): string {
166  $usedOptions = ParserOptions::allCacheVaryingOptions();
167 
168  // revId may be null.
169  $revId = (string)$revision->getId();
170  $hash = $options->optionsHash( $usedOptions );
171  return $this->cache->makeKey( $this->name, $revId, $hash );
172  }
173 
183  public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
184  if ( $this->cacheExpiry <= 0 ) {
185  // disabled
186  return false;
187  }
188 
189  if ( !$parserOptions->isSafeToCache() ) {
190  $this->incrementStats( 'miss.unsafe' );
191  return false;
192  }
193 
194  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
195  $json = $this->cache->get( $cacheKey );
196 
197  if ( $json === false ) {
198  $this->incrementStats( 'miss.absent' );
199  return false;
200  }
201 
202  $output = $this->restoreFromJson( $json, $cacheKey, ParserOutput::class );
203  if ( $output === null ) {
204  $this->incrementStats( 'miss.unserialize' );
205  return false;
206  }
207 
208  $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() );
209  $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch );
210  $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry );
211 
212  if ( $cacheTime < $expiryTime ) {
213  $this->incrementStats( 'miss.expired' );
214  return false;
215  }
216 
217  $this->logger->debug( 'old-revision cache hit' );
218  $this->incrementStats( 'hit' );
219  return $output;
220  }
221 
228  public function save(
229  ParserOutput $output,
230  RevisionRecord $revision,
231  ParserOptions $parserOptions,
232  string $cacheTime = null
233  ) {
234  if ( !$output->hasText() ) {
235  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
236  }
237 
238  if ( $this->cacheExpiry <= 0 ) {
239  // disabled
240  return;
241  }
242 
243  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
244 
245  $output->setCacheTime( $cacheTime ?: wfTimestampNow() );
246  $output->setCacheRevisionId( $revision->getId() );
247 
248  // Save the timestamp so that we don't have to load the revision row on view
249  $output->setTimestamp( $revision->getTimestamp() );
250 
251  $msg = "Saved in RevisionOutputCache with key $cacheKey" .
252  " and timestamp $cacheTime" .
253  " and revision id {$revision->getId()}.";
254 
255  $output->addCacheMessage( $msg );
256 
257  // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
258  $output->updateCacheExpiry( $this->cacheExpiry );
259 
260  $expiry = $output->getCacheExpiry();
261  if ( $expiry <= 0 ) {
262  $this->incrementStats( 'save.uncacheable' );
263  return;
264  }
265 
266  if ( !$parserOptions->isSafeToCache() ) {
267  $this->incrementStats( 'save.unsafe' );
268  return;
269  }
270 
271  $json = $this->encodeAsJson( $output, $cacheKey );
272  if ( $json === null ) {
273  $this->incrementStats( 'save.nonserializable' );
274  return;
275  }
276 
277  $this->cache->set( $cacheKey, $json, $expiry );
278  $this->incrementStats( 'save.success' );
279  }
280 
287  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
288  try {
290  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
291  return $obj;
292  } catch ( InvalidArgumentException $e ) {
293  $this->logger->error( 'Unable to unserialize JSON', [
294  'name' => $this->name,
295  'cache_key' => $key,
296  'message' => $e->getMessage()
297  ] );
298  return null;
299  }
300  }
301 
307  private function encodeAsJson( CacheTime $obj, string $key ) {
308  try {
309  return $this->jsonCodec->serialize( $obj );
310  } catch ( InvalidArgumentException $e ) {
311  $this->logger->error( 'Unable to serialize JSON', [
312  'name' => $this->name,
313  'cache_key' => $key,
314  'message' => $e->getMessage(),
315  ] );
316  return null;
317  }
318  }
319 }
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Parser cache specific expiry check.
Definition: CacheTime.php:38
setCacheRevisionId( $id)
Definition: CacheTime.php:107
updateCacheExpiry( $seconds)
Sets the number of seconds after which this object should expire.
Definition: CacheTime.php:128
setCacheTime( $t)
setCacheTime() sets the timestamp expressing when the page has been rendered.
Definition: CacheTime.php:82
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:145
Cache for ParserOutput objects.
makeParserOutputKeyOptionalRevId(RevisionRecord $revision, ParserOptions $options, array $usedOptions=null)
Get a key that will be used for locks or pool counter.
__construct(string $name, WANObjectCache $cache, int $cacheExpiry, string $cacheEpoch, JsonCodec $jsonCodec, IBufferingStatsdDataFactory $stats, LoggerInterface $logger)
save(ParserOutput $output, RevisionRecord $revision, ParserOptions $parserOptions, string $cacheTime=null)
makeParserOutputKey(RevisionRecord $revision, ParserOptions $options, array $usedOptions=null)
Get a key that will be used by this cache to store the content for a given page considering the given...
Page revision base class.
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:48
Set options of the Parser.
isSafeToCache(array $usedOptions=null)
Test whether these options are safe to cache.
optionsHash( $forOptions, $title=null)
Generate a hash string with the values set on these ParserOptions for the keys given in the array.
hasText()
Returns true if text was passed to the constructor, or set using setText().
addCacheMessage(string $msg)
Adds a comment notice about cache state to the text of the page.
setTimestamp( $timestamp)
Multi-datacenter aware caching interface.
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.