MediaWiki 1.40.4
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
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: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.