MediaWiki 1.43.1
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.
getCacheExpiry()
Returns 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().
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.
StatsFactory Implementation.
Class for getting statistically unique IDs without a central coordinator.