MediaWiki master
RevisionOutputCache.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Parser;
25
26use InvalidArgumentException;
27use JsonException;
31use Psr\Log\LoggerInterface;
35
44
46 private $name;
47
49 private $cache;
50
56 private $cacheEpoch;
57
63 private $cacheExpiry;
64
66 private $jsonCodec;
67
69 private $stats;
70
72 private $logger;
73
74 private GlobalIdGenerator $globalIdGenerator;
75
86 public function __construct(
87 string $name,
88 WANObjectCache $cache,
89 int $cacheExpiry,
90 string $cacheEpoch,
91 JsonCodec $jsonCodec,
92 StatsFactory $stats,
93 LoggerInterface $logger,
94 GlobalIdGenerator $globalIdGenerator
95 ) {
96 $this->name = $name;
97 $this->cache = $cache;
98 $this->cacheExpiry = $cacheExpiry;
99 $this->cacheEpoch = $cacheEpoch;
100 $this->jsonCodec = $jsonCodec;
101 $this->stats = $stats;
102 $this->logger = $logger;
103 $this->globalIdGenerator = $globalIdGenerator;
104 }
105
110 private function incrementStats( string $status, ?string $reason = null ) {
111 $metricSuffix = $reason ? "{$status}_{$reason}" : $status;
112
113 $this->stats->getCounter( 'RevisionOutputCache_operation_total' )
114 ->setLabel( 'name', $this->name )
115 ->setLabel( 'status', $status )
116 ->setLabel( 'reason', $reason ?: 'n/a' )
117 ->copyToStatsdAt( "RevisionOutputCache.{$this->name}.{$metricSuffix}" )
118 ->increment();
119 }
120
139 public function makeParserOutputKey(
140 RevisionRecord $revision,
141 ParserOptions $options,
142 ?array $usedOptions = null
143 ): string {
144 $usedOptions = ParserOptions::allCacheVaryingOptions();
145
146 $revId = $revision->getId();
147 if ( !$revId ) {
148 // If RevId is null, this would probably be unsafe to use as a cache key.
149 throw new InvalidArgumentException( "Revision must have an id number" );
150 }
151 $hash = $options->optionsHash( $usedOptions );
152 return $this->cache->makeKey( $this->name, $revId, $hash );
153 }
154
173 RevisionRecord $revision,
174 ParserOptions $options,
175 ?array $usedOptions = null
176 ): string {
177 $usedOptions = ParserOptions::allCacheVaryingOptions();
178
179 // revId may be null.
180 $revId = (string)$revision->getId();
181 $hash = $options->optionsHash( $usedOptions );
182 return $this->cache->makeKey( $this->name, $revId, $hash );
183 }
184
194 public function get( RevisionRecord $revision, ParserOptions $parserOptions ) {
195 if ( $this->cacheExpiry <= 0 ) {
196 // disabled
197 return false;
198 }
199
200 if ( !$parserOptions->isSafeToCache() ) {
201 $this->incrementStats( 'miss', 'unsafe' );
202 return false;
203 }
204
205 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
206 $json = $this->cache->get( $cacheKey );
207
208 if ( $json === false ) {
209 $this->incrementStats( 'miss', 'absent' );
210 return false;
211 }
212
213 $output = $this->restoreFromJson( $json, $cacheKey, ParserOutput::class );
214 if ( $output === null ) {
215 $this->incrementStats( 'miss', 'unserialize' );
216 return false;
217 }
218
219 $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() );
220 $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch );
221 $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry );
222
223 if ( $cacheTime < $expiryTime ) {
224 $this->incrementStats( 'miss', 'expired' );
225 return false;
226 }
227
228 $this->logger->debug( 'old-revision cache hit' );
229 $this->incrementStats( 'hit' );
230 return $output;
231 }
232
239 public function save(
240 ParserOutput $output,
241 RevisionRecord $revision,
242 ParserOptions $parserOptions,
243 ?string $cacheTime = null
244 ) {
245 if ( !$output->hasText() ) {
246 throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' );
247 }
248
249 if ( $this->cacheExpiry <= 0 ) {
250 // disabled
251 return;
252 }
253
254 $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions );
255
256 // Ensure cache properties are set in the ParserOutput
257 // T350538: These should be turned into assertions that the
258 // properties are already present (and the $cacheTime argument
259 // removed).
260 if ( $cacheTime ) {
261 $output->setCacheTime( $cacheTime );
262 } else {
263 $cacheTime = $output->getCacheTime();
264 }
265 if ( !$output->getCacheRevisionId() ) {
266 $output->setCacheRevisionId( $revision->getId() );
267 }
268 if ( !$output->getRenderId() ) {
269 $output->setRenderId( $this->globalIdGenerator->newUUIDv1() );
270 }
271 if ( !$output->getRevisionTimestamp() ) {
272 $output->setRevisionTimestamp( $revision->getTimestamp() );
273 }
274
275 $msg = "Saved in RevisionOutputCache with key $cacheKey" .
276 " and timestamp $cacheTime" .
277 " and revision id {$revision->getId()}.";
278
279 $output->addCacheMessage( $msg );
280
281 // The ParserOutput might be dynamic and have been marked uncacheable by the parser.
282 $output->updateCacheExpiry( $this->cacheExpiry );
283
284 $expiry = $output->getCacheExpiry();
285 if ( $expiry <= 0 ) {
286 $this->incrementStats( 'save', 'uncacheable' );
287 return;
288 }
289
290 if ( !$parserOptions->isSafeToCache() ) {
291 $this->incrementStats( 'save', 'unsafe' );
292 return;
293 }
294
295 $json = $this->encodeAsJson( $output, $cacheKey );
296 if ( $json === null ) {
297 $this->incrementStats( 'save', 'nonserializable' );
298 return;
299 }
300
301 $this->cache->set( $cacheKey, $json, $expiry );
302 $this->incrementStats( 'save', 'success' );
303 }
304
311 private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) {
312 try {
314 $obj = $this->jsonCodec->deserialize( $jsonData, $expectedClass );
315 return $obj;
316 } catch ( JsonException $e ) {
317 $this->logger->error( 'Unable to deserialize JSON', [
318 'name' => $this->name,
319 'cache_key' => $key,
320 'message' => $e->getMessage()
321 ] );
322 return null;
323 }
324 }
325
331 private function encodeAsJson( CacheTime $obj, string $key ) {
332 try {
333 return $this->jsonCodec->serialize( $obj );
334 } catch ( JsonException $e ) {
335 $this->logger->error( 'Unable to serialize JSON', [
336 'name' => $this->name,
337 'cache_key' => $key,
338 'message' => $e->getMessage(),
339 ] );
340 return null;
341 }
342 }
343}
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.
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.
getId( $wikiId=self::LOCAL)
Get revision ID.
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.