MediaWiki REL1_39
RevisionOutputCache.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Parser;
25
26use CacheTime;
28use InvalidArgumentException;
31use MWTimestamp;
33use ParserOutput;
34use Psr\Log\LoggerInterface;
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)
updateCacheExpiry( $seconds)
Sets the number of seconds after which this object should expire.
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.
Library for creating and parsing MW-style timestamps.
Cache for ParserOutput objects.
save(ParserOutput $output, RevisionRecord $revision, ParserOptions $parserOptions, string $cacheTime=null)
__construct(string $name, WANObjectCache $cache, int $cacheExpiry, string $cacheEpoch, JsonCodec $jsonCodec, IBufferingStatsdDataFactory $stats, LoggerInterface $logger)
makeParserOutputKeyOptionalRevId(RevisionRecord $revision, ParserOptions $options, array $usedOptions=null)
Get a key that will be used for locks or pool counter.
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.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getId( $wikiId=self::LOCAL)
Get revision ID.
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