MediaWiki master
RevisionOutputCache.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Parser;
25
26use InvalidArgumentException;
27use JsonException;
32use 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
75 private GlobalIdGenerator $globalIdGenerator;
76
87 public function __construct(
88 string $name,
89 WANObjectCache $cache,
90 int $cacheExpiry,
91 string $cacheEpoch,
92 JsonCodec $jsonCodec,
93 StatsFactory $stats,
94 LoggerInterface $logger,
95 GlobalIdGenerator $globalIdGenerator
96 ) {
97 $this->name = $name;
98 $this->cache = $cache;
99 $this->cacheExpiry = $cacheExpiry;
100 $this->cacheEpoch = $cacheEpoch;
101 $this->jsonCodec = $jsonCodec;
102 $this->stats = $stats;
103 $this->logger = $logger;
104 $this->globalIdGenerator = $globalIdGenerator;
105 }
106
111 private function getContentModelFromRevision( RevisionRecord $revision ) {
112 if ( !$revision->hasSlot( SlotRecord::MAIN ) ) {
113 return 'missing';
114 }
115 return str_replace( '.', '_', $revision->getMainContentModel() );
116 }
117
123 private function incrementStats( RevisionRecord $revision, string $status, ?string $reason = null ) {
124 $contentModel = $this->getContentModelFromRevision( $revision );
125 $metricSuffix = $reason ? "{$status}_{$reason}" : $status;
126
127 $this->stats->getCounter( 'RevisionOutputCache_operation_total' )
128 ->setLabel( 'name', $this->name )
129 ->setLabel( 'contentModel', $contentModel )
130 ->setLabel( 'status', $status )
131 ->setLabel( 'reason', $reason ?: 'n/a' )
132 ->copyToStatsdAt( "RevisionOutputCache.{$this->name}.{$metricSuffix}" )
133 ->increment();
134 }
135
140 private function incrementRenderReasonStats( RevisionRecord $revision, $renderReason ) {
141 $contentModel = $this->getContentModelFromRevision( $revision );
142 $renderReason = preg_replace( '/\W+/', '_', $renderReason );
143
144 $this->stats->getCounter( 'RevisionOutputCache_render_total' )
145 ->setLabel( 'name', $this->name )
146 ->setLabel( 'contentModel', $contentModel )
147 ->setLabel( 'reason', $renderReason )
148 ->increment();
149 }
150
169 public function makeParserOutputKey(
170 RevisionRecord $revision,
171 ParserOptions $options,
172 ?array $usedOptions = null
173 ): string {
174 $usedOptions = ParserOptions::allCacheVaryingOptions();
175
176 $revId = $revision->getId();
177 if ( !$revId ) {
178 // If RevId is null, this would probably be unsafe to use as a cache key.
179 throw new InvalidArgumentException( "Revision must have an id number" );
180 }
181 $hash = $options->optionsHash( $usedOptions );
182 return $this->cache->makeKey( $this->name, $revId, $hash );
183 }
184
203 RevisionRecord $revision,
204 ParserOptions $options,
205 ?array $usedOptions = null
206 ): string {
207 $usedOptions = ParserOptions::allCacheVaryingOptions();
208
209 // revId may be null.
210 $revId = (string)$revision->getId();
211 $hash = $options->optionsHash( $usedOptions );
212 return $this->cache->makeKey( $this->name, $revId, $hash );
213 }
214
224 public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
225 if ( $this->cacheExpiry <= 0 ) {
226 // disabled
227 return false;
228 }
229
230 if ( !$parserOptions->isSafeToCache() ) {
231 $this->incrementStats( $revision, 'miss', 'unsafe' );
232 return false;
233 }
234
235 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
236 $json = $this->cache->get( $cacheKey );
237
238 if ( $json === false ) {
239 $this->incrementStats( $revision, 'miss', 'absent' );
240 return false;
241 }
242
243 $output = $this->restoreFromJson( $json, $cacheKey );
244 if ( $output === null ) {
245 $this->incrementStats( $revision, 'miss', 'unserialize' );
246 return false;
247 }
248
249 $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() );
250 $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch );
251 $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry );
252
253 if ( $cacheTime < $expiryTime ) {
254 $this->incrementStats( $revision, 'miss', 'expired' );
255 return false;
256 }
257
258 $this->logger->debug( 'old-revision cache hit' );
259 $this->incrementStats( $revision, 'hit' );
260 return $output;
261 }
262
269 public function save(
270 ParserOutput $output,
271 RevisionRecord $revision,
272 ParserOptions $parserOptions,
273 ?string $cacheTime = null
274 ) {
275 if ( !$output->hasText() ) {
276 throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
277 }
278
279 if ( $this->cacheExpiry <= 0 ) {
280 // disabled
281 return;
282 }
283
284 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
285
286 // Ensure cache properties are set in the ParserOutput
287 // T350538: These should be turned into assertions that the
288 // properties are already present (and the $cacheTime argument
289 // removed).
290 if ( $cacheTime ) {
291 $output->setCacheTime( $cacheTime );
292 } else {
293 $cacheTime = $output->getCacheTime();
294 }
295 if ( !$output->getCacheRevisionId() ) {
296 $output->setCacheRevisionId( $revision->getId() );
297 }
298 if ( !$output->getRenderId() ) {
299 $output->setRenderId( $this->globalIdGenerator->newUUIDv1() );
300 }
301 if ( !$output->getRevisionTimestamp() ) {
302 $output->setRevisionTimestamp( $revision->getTimestamp() );
303 }
304
305 $msg = "Saved in RevisionOutputCache with key $cacheKey" .
306 " and timestamp $cacheTime" .
307 " and revision id {$revision->getId()}.";
308
309 $output->addCacheMessage( $msg );
310
311 // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
312 $output->updateCacheExpiry( $this->cacheExpiry );
313
314 $expiry = $output->getCacheExpiry();
315 if ( $expiry <= 0 ) {
316 $this->incrementStats( $revision, 'save', 'uncacheable' );
317 return;
318 }
319
320 if ( !$parserOptions->isSafeToCache() ) {
321 $this->incrementStats( $revision, 'save', 'unsafe' );
322 return;
323 }
324
325 $json = $this->encodeAsJson( $output, $cacheKey );
326 if ( $json === null ) {
327 $this->incrementStats( $revision, 'save', 'nonserializable' );
328 return;
329 }
330
331 $this->cache->set( $cacheKey, $json, $expiry );
332 $this->incrementStats( $revision, 'save', 'success' );
333 $this->incrementRenderReasonStats( $revision, $parserOptions->getRenderReason() );
334 }
335
341 private function restoreFromJson( string $jsonData, string $key ) {
342 try {
344 $obj = $this->jsonCodec->deserialize( $jsonData, ParserOutput::class );
345 return $obj;
346 } catch ( JsonException $e ) {
347 $this->logger->error( 'Unable to deserialize JSON', [
348 'name' => $this->name,
349 'cache_key' => $key,
350 'message' => $e->getMessage()
351 ] );
352 return null;
353 }
354 }
355
361 private function encodeAsJson( CacheTime $obj, string $key ) {
362 try {
363 return $this->jsonCodec->serialize( $obj );
364 } catch ( JsonException $e ) {
365 $this->logger->error( 'Unable to serialize JSON', [
366 'name' => $this->name,
367 'cache_key' => $key,
368 'message' => $e->getMessage(),
369 ] );
370 return null;
371 }
372 }
373}
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:90
Set options of the Parser.
optionsHash( $forOptions, $title=null)
Generate a hash string with the values set on these ParserOptions for the keys given in the array.
getRenderReason()
Returns reason for rendering the content.
isSafeToCache(?array $usedOptions=null)
Test whether these options are safe to cache.
ParserOutput is a rendering of a Content object or a message.
getRenderId()
Return the unique rendering id for this ParserOutput.
setRenderId(string $renderId)
Store a unique rendering id for this ParserOutput.
setRevisionTimestamp(?string $timestamp)
addCacheMessage(string $msg)
Adds a comment notice about cache state to the text of the page.
hasText()
Returns true if text was passed to the constructor, or set using setText().
getCacheExpiry()
Returns the number of seconds after which this object should expire.
Cache for ParserOutput objects.
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...
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, StatsFactory $stats, LoggerInterface $logger, GlobalIdGenerator $globalIdGenerator)
save(ParserOutput $output, RevisionRecord $revision, ParserOptions $parserOptions, ?string $cacheTime=null)
Page revision base class.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getMainContentModel()
Returns the content model of the main slot of this revision.
hasSlot( $role)
Returns whether the given slot is defined in this revision.
getId( $wikiId=self::LOCAL)
Get revision ID.
Value object representing a content slot associated with a page revision.
Library for creating and parsing MW-style timestamps.
Multi-datacenter aware caching interface.
This is the primary interface for validating metrics definitions, caching defined metrics,...
Class for getting statistically unique IDs without a central coordinator.