MediaWiki master
RevisionOutputCache.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Parser;
11
12use InvalidArgumentException;
13use JsonException;
18use Psr\Log\LoggerInterface;
21use Wikimedia\Timestamp\TimestampFormat as TS;
23
32
34 private $name;
35
41 private $cacheEpoch;
42
48 private $cacheExpiry;
49
60 public function __construct(
61 string $name,
62 private readonly WANObjectCache $cache,
63 int $cacheExpiry,
64 string $cacheEpoch,
65 private readonly JsonCodec $jsonCodec,
66 private readonly StatsFactory $stats,
67 private readonly LoggerInterface $logger,
68 private readonly GlobalIdGenerator $globalIdGenerator,
69 ) {
70 $this->name = $name;
71 $this->cacheExpiry = $cacheExpiry;
72 $this->cacheEpoch = $cacheEpoch;
73 }
74
79 private function getContentModelFromRevision( RevisionRecord $revision ) {
80 if ( !$revision->hasSlot( SlotRecord::MAIN ) ) {
81 return 'missing';
82 }
83 return str_replace( '.', '_', $revision->getMainContentModel() );
84 }
85
91 private function incrementStats( RevisionRecord $revision, string $status, ?string $reason = null ) {
92 $contentModel = $this->getContentModelFromRevision( $revision );
93
94 $this->stats->getCounter( 'RevisionOutputCache_operation_total' )
95 ->setLabel( 'name', $this->name )
96 ->setLabel( 'contentModel', $contentModel )
97 ->setLabel( 'status', $status )
98 ->setLabel( 'reason', $reason ?: 'n/a' )
99 ->increment();
100 }
101
106 private function incrementRenderReasonStats( RevisionRecord $revision, $renderReason ) {
107 $contentModel = $this->getContentModelFromRevision( $revision );
108 $renderReason = preg_replace( '/\W+/', '_', $renderReason );
109
110 $this->stats->getCounter( 'RevisionOutputCache_render_total' )
111 ->setLabel( 'name', $this->name )
112 ->setLabel( 'contentModel', $contentModel )
113 ->setLabel( 'reason', $renderReason )
114 ->increment();
115 }
116
135 public function makeParserOutputKey(
136 RevisionRecord $revision,
137 ParserOptions $options,
138 ?array $usedOptions = null
139 ): string {
140 $usedOptions = ParserOptions::allCacheVaryingOptions();
141
142 $revId = $revision->getId();
143 if ( !$revId ) {
144 // If RevId is null, this would probably be unsafe to use as a cache key.
145 throw new InvalidArgumentException( "Revision must have an id number" );
146 }
147 $hash = $options->optionsHash( $usedOptions );
148 return $this->cache->makeKey( $this->name, $revId, $hash );
149 }
150
169 RevisionRecord $revision,
170 ParserOptions $options,
171 ?array $usedOptions = null
172 ): string {
173 $usedOptions = ParserOptions::allCacheVaryingOptions();
174
175 // revId may be null.
176 $revId = (string)$revision->getId();
177 $hash = $options->optionsHash( $usedOptions );
178 return $this->cache->makeKey( $this->name, $revId, $hash );
179 }
180
190 public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
191 if ( $this->cacheExpiry <= 0 ) {
192 // disabled
193 return false;
194 }
195
196 if ( !$parserOptions->isSafeToCache() ) {
197 $this->incrementStats( $revision, 'miss', 'unsafe' );
198 return false;
199 }
200
201 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
202 $json = $this->cache->get( $cacheKey );
203
204 if ( $json === false ) {
205 $this->incrementStats( $revision, 'miss', 'absent' );
206 return false;
207 }
208
209 $output = $this->restoreFromJson( $json, $cacheKey );
210 if ( $output === null ) {
211 $this->incrementStats( $revision, 'miss', 'unserialize' );
212 return false;
213 }
214
215 $cacheTime = (int)MWTimestamp::convert( TS::UNIX, $output->getCacheTime() );
216 $expiryTime = (int)MWTimestamp::convert( TS::UNIX, $this->cacheEpoch );
217 $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS::UNIX ) - $this->cacheExpiry );
218
219 if ( $cacheTime < $expiryTime ) {
220 $this->incrementStats( $revision, 'miss', 'expired' );
221 return false;
222 }
223
224 $this->logger->debug( 'old-revision cache hit' );
225 $this->incrementStats( $revision, 'hit' );
226 return $output;
227 }
228
235 public function save(
236 ParserOutput $output,
237 RevisionRecord $revision,
238 ParserOptions $parserOptions,
239 ?string $cacheTime = null
240 ) {
241 if ( !$output->hasText() ) {
242 throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
243 }
244
245 if ( $this->cacheExpiry <= 0 ) {
246 // disabled
247 return;
248 }
249
250 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
251
252 // Ensure cache properties are set in the ParserOutput
253 // T350538: These should be turned into assertions that the
254 // properties are already present (and the $cacheTime argument
255 // removed).
256 if ( $cacheTime ) {
257 $output->setCacheTime( $cacheTime );
258 } else {
259 $cacheTime = $output->getCacheTime();
260 }
261 if ( !$output->getCacheRevisionId() ) {
262 $output->setCacheRevisionId( $revision->getId() );
263 }
264 if ( !$output->getRenderId() ) {
265 $output->setRenderId( $this->globalIdGenerator->newUUIDv1() );
266 }
267 if ( !$output->getRevisionTimestamp() ) {
268 $output->setRevisionTimestamp( $revision->getTimestamp() );
269 }
270
271 $msg = "Saved in RevisionOutputCache with key $cacheKey" .
272 " and timestamp $cacheTime" .
273 " and revision id {$revision->getId()}.";
274
275 $output->addCacheMessage( $msg );
276
277 // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
278 $output->updateCacheExpiry( $this->cacheExpiry );
279
280 $expiry = $output->getCacheExpiry();
281 if ( $expiry <= 0 ) {
282 $this->incrementStats( $revision, 'save', 'uncacheable' );
283 return;
284 }
285
286 if ( !$parserOptions->isSafeToCache() ) {
287 $this->incrementStats( $revision, 'save', 'unsafe' );
288 return;
289 }
290
291 $json = $this->encodeAsJson( $output, $cacheKey );
292 if ( $json === null ) {
293 $this->incrementStats( $revision, 'save', 'nonserializable' );
294 return;
295 }
296
297 $this->cache->set( $cacheKey, $json, $expiry );
298 $this->incrementStats( $revision, 'save', 'success' );
299 $this->incrementRenderReasonStats( $revision, $parserOptions->getRenderReason() );
300 }
301
307 private function restoreFromJson( string $jsonData, string $key ) {
308 try {
310 $obj = $this->jsonCodec->deserialize( $jsonData, ParserOutput::class );
311 return $obj;
312 } catch ( JsonException $e ) {
313 $this->logger->error( 'Unable to deserialize JSON', [
314 'name' => $this->name,
315 'cache_key' => $key,
316 'message' => $e->getMessage()
317 ] );
318 return null;
319 }
320 }
321
327 private function encodeAsJson( CacheTime $obj, string $key ) {
328 try {
329 return $this->jsonCodec->serialize( $obj );
330 } catch ( JsonException $e ) {
331 $this->logger->error( 'Unable to serialize JSON', [
332 'name' => $this->name,
333 'cache_key' => $key,
334 'message' => $e->getMessage(),
335 ] );
336 return null;
337 }
338 }
339}
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:74
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 the set of provided 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...
__construct(string $name, private readonly WANObjectCache $cache, int $cacheExpiry, string $cacheEpoch, private readonly JsonCodec $jsonCodec, private readonly StatsFactory $stats, private readonly LoggerInterface $logger, private readonly GlobalIdGenerator $globalIdGenerator,)
makeParserOutputKeyOptionalRevId(RevisionRecord $revision, ParserOptions $options, ?array $usedOptions=null)
Get a key that will be used for locks or pool counter.
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.