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,
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 
126  public function makeParserOutputKey(
127  RevisionRecord $revision,
128  ParserOptions $options,
129  array $usedOptions = null
130  ): string {
131  $usedOptions = ParserOptions::allCacheVaryingOptions();
132 
133  $revId = $revision->getId();
134  $hash = $options->optionsHash( $usedOptions );
135 
136  return $this->cache->makeKey( $this->name, $revId, $hash );
137  }
138 
148  public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
149  if ( $this->cacheExpiry <= 0 ) {
150  // disabled
151  return false;
152  }
153 
154  if ( !$parserOptions->isSafeToCache() ) {
155  $this->incrementStats( $revision, 'miss.unsafe' );
156  return false;
157  }
158 
159  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
160  $json = $this->cache->get( $cacheKey );
161 
162  if ( $json === false ) {
163  $this->incrementStats( $revision, 'miss.absent' );
164  return false;
165  }
166 
167  $output = $this->restoreFromJson( $json, $cacheKey, ParserOutput::class );
168  if ( $output === null ) {
169  $this->incrementStats( $revision, 'miss.unserialize' );
170  return false;
171  }
172 
173  $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() );
174  $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch );
175  $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry );
176 
177  if ( $cacheTime < $expiryTime ) {
178  $this->incrementStats( $revision, 'miss.expired' );
179  return false;
180  }
181 
182  $this->logger->debug( 'old-revision cache hit' );
183  $this->incrementStats( $revision, 'hit' );
184  return $output;
185  }
186 
193  public function save(
194  ParserOutput $output,
195  RevisionRecord $revision,
196  ParserOptions $parserOptions,
197  string $cacheTime = null
198  ) {
199  if ( !$output->hasText() ) {
200  throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
201  }
202 
203  if ( $this->cacheExpiry <= 0 ) {
204  // disabled
205  return;
206  }
207 
208  $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
209 
210  $output->setCacheTime( $cacheTime ?: wfTimestampNow() );
211  $output->setCacheRevisionId( $revision->getId() );
212 
213  // Save the timestamp so that we don't have to load the revision row on view
214  $output->setTimestamp( $revision->getTimestamp() );
215 
216  $msg = "Saved in RevisionOutputCache with key $cacheKey" .
217  " and timestamp $cacheTime" .
218  " and revision id {$revision->getId()}.";
219 
220  $output->addCacheMessage( $msg );
221 
222  // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
223  $output->updateCacheExpiry( $this->cacheExpiry );
224 
225  $expiry = $output->getCacheExpiry();
226  if ( $expiry <= 0 ) {
227  $this->incrementStats( $revision, 'save.uncacheable' );
228  return;
229  }
230 
231  if ( !$parserOptions->isSafeToCache() ) {
232  $this->incrementStats( $revision, 'save.unsafe' );
233  return;
234  }
235 
236  $json = $this->encodeAsJson( $output, $cacheKey );
237  if ( $json === null ) {
238  $this->incrementStats( $revision, 'save.nonserializable' );
239  return;
240  }
241 
242  $this->cache->set( $cacheKey, $json, $expiry );
243  $this->incrementStats( $revision, 'save.success' );
244  }
245 
252  private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
253  try {
255  $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass );
256  return $obj;
257  } catch ( InvalidArgumentException $e ) {
258  $this->logger->error( 'Unable to unserialize JSON', [
259  'name' => $this->name,
260  'cache_key' => $key,
261  'message' => $e->getMessage()
262  ] );
263  return null;
264  }
265  }
266 
272  private function encodeAsJson( CacheTime $obj, string $key ) {
273  try {
274  return $this->jsonCodec->serialize( $obj );
275  } catch ( InvalidArgumentException $e ) {
276  $this->logger->error( 'Unable to serialize JSON', [
277  'name' => $this->name,
278  'cache_key' => $key,
279  'message' => $e->getMessage(),
280  ] );
281  return null;
282  }
283  }
284 }
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.
encodeAsJson(CacheTime $obj, string $key)
incrementStats(RevisionRecord $revision, string $metricSuffix)
__construct(string $name, WANObjectCache $cache, int $cacheExpiry, string $cacheEpoch, JsonCodec $jsonCodec, IBufferingStatsdDataFactory $stats, LoggerInterface $logger)
IBufferingStatsdDataFactory $stats
restoreFromJson(string $jsonData, string $key, string $expectedClass)
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...
string $name
The name of this cache.
string $cacheEpoch
Anything cached prior to this is invalidated.
int $cacheExpiry
Expiry time for cache entries.
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.