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