MediaWiki  master
RevisionOutputCache.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Parser;
25 
26 use CacheTime;
28 use InvalidArgumentException;
31 use MWTimestamp;
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 
106  private function incrementStats( RevisionRecord $revision, string $metricSuffix ) {
107  $metricSuffix = str_replace( '.', '_', $metricSuffix );
108  $this->stats->increment( "RevisionOutputCache.{$this->name}.{$metricSuffix}" );
109  }
110 
129  public function makeParserOutputKey(
130  RevisionRecord $revision,
131  ParserOptions $options,
132  array $usedOptions = null
133  ): string {
134  $usedOptions = ParserOptions::allCacheVaryingOptions();
135 
136  $revId = $revision->getId();
137  if ( !$revId ) {
138  // If RevId is null, this would probably be unsafe to use as a cache key.
139  throw new InvalidArgumentException( "Revision must have an id number" );
140  }
141  $hash = $options->optionsHash( $usedOptions );
142  return $this->cache->makeKey( $this->name, $revId, $hash );
143  }
144 
163  RevisionRecord $revision,
164  ParserOptions $options,
165  array $usedOptions = null
166  ): string {
167  $usedOptions = ParserOptions::allCacheVaryingOptions();
168 
169  // revId may be null.
170  $revId = (string)$revision->getId();
171  $hash = $options->optionsHash( $usedOptions );
172  return $this->cache->makeKey( $this->name, $revId, $hash );
173  }
174 
184  public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
185  if ( $this->cacheExpiry <= 0 ) {
186  // disabled
187  return false;
188  }
189 
190  if ( !$parserOptions->isSafeToCache() ) {
191  $this->incrementStats( $revision, 'miss.unsafe' );
192  return false;
193  }
194 
195  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
196  $json = $this->cache->get( $cacheKey );
197 
198  if ( $json === false ) {
199  $this->incrementStats( $revision, 'miss.absent' );
200  return false;
201  }
202 
203  $output = $this->restoreFromJson( $json, $cacheKey, ParserOutput::class );
204  if ( $output === null ) {
205  $this->incrementStats( $revision, 'miss.unserialize' );
206  return false;
207  }
208 
209  $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() );
210  $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch );
211  $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry );
212 
213  if ( $cacheTime < $expiryTime ) {
214  $this->incrementStats( $revision, 'miss.expired' );
215  return false;
216  }
217 
218  $this->logger->debug( 'old-revision cache hit' );
219  $this->incrementStats( $revision, 'hit' );
220  return $output;
221  }
222 
229  public function save(
230  ParserOutput $output,
231  RevisionRecord $revision,
232  ParserOptions $parserOptions,
233  string $cacheTime = null
234  ) {
235  if ( !$output->hasText() ) {
236  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
237  }
238 
239  if ( $this->cacheExpiry <= 0 ) {
240  // disabled
241  return;
242  }
243 
244  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
245 
246  $output->setCacheTime( $cacheTime ?: wfTimestampNow() );
247  $output->setCacheRevisionId( $revision->getId() );
248 
249  // Save the timestamp so that we don't have to load the revision row on view
250  $output->setTimestamp( $revision->getTimestamp() );
251 
252  $msg = "Saved in RevisionOutputCache with key $cacheKey" .
253  " and timestamp $cacheTime" .
254  " and revision id {$revision->getId()}.";
255 
256  $output->addCacheMessage( $msg );
257 
258  // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
259  $output->updateCacheExpiry( $this->cacheExpiry );
260 
261  $expiry = $output->getCacheExpiry();
262  if ( $expiry <= 0 ) {
263  $this->incrementStats( $revision, 'save.uncacheable' );
264  return;
265  }
266 
267  if ( !$parserOptions->isSafeToCache() ) {
268  $this->incrementStats( $revision, 'save.unsafe' );
269  return;
270  }
271 
272  $json = $this->encodeAsJson( $output, $cacheKey );
273  if ( $json === null ) {
274  $this->incrementStats( $revision, 'save.nonserializable' );
275  return;
276  }
277 
278  $this->cache->set( $cacheKey, $json, $expiry );
279  $this->incrementStats( $revision, 'save.success' );
280  }
281 
288  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
289  try {
291  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
292  return $obj;
293  } catch ( InvalidArgumentException $e ) {
294  $this->logger->error( 'Unable to unserialize JSON', [
295  'name' => $this->name,
296  'cache_key' => $key,
297  'message' => $e->getMessage()
298  ] );
299  return null;
300  }
301  }
302 
308  private function encodeAsJson( CacheTime $obj, string $key ) {
309  try {
310  return $this->jsonCodec->serialize( $obj );
311  } catch ( InvalidArgumentException $e ) {
312  $this->logger->error( 'Unable to serialize JSON', [
313  'name' => $this->name,
314  'cache_key' => $key,
315  'message' => $e->getMessage(),
316  ] );
317  return null;
318  }
319  }
320 }
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Parser cache specific expiry check.
Definition: CacheTime.php:37
setCacheRevisionId( $id)
Definition: CacheTime.php:106
updateCacheExpiry( $seconds)
Sets the number of seconds after which this object should expire.
Definition: CacheTime.php:127
setCacheTime( $t)
setCacheTime() sets the timestamp expressing when the page has been rendered.
Definition: CacheTime.php:81
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Definition: CacheTime.php:144
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:39
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.
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.
$cache
Definition: mcc.php:33