name = $name; $this->cache = $cache; $this->cacheExpiry = $cacheExpiry; $this->cacheEpoch = $cacheEpoch; $this->jsonCodec = $jsonCodec; $this->stats = $stats; $this->logger = $logger; } /** * @param RevisionRecord $revision * @param string $metricSuffix */ private function incrementStats( RevisionRecord $revision, string $metricSuffix ) { $metricSuffix = str_replace( '.', '_', $metricSuffix ); $this->stats->increment( "RevisionOutputCache.{$this->name}.{$metricSuffix}" ); } /** * Get a key that will be used by this cache to store the content * for a given page considering the given options and the array of * used options. * * @warning The exact format of the key is considered internal and is subject * to change, thus should not be used as storage or long-term caching key. * This is intended to be used for logging or keying something transient. * * @param RevisionRecord $revision * @param ParserOptions $options * @param array|null $usedOptions currently ignored * @return string * @internal */ public function makeParserOutputKey( RevisionRecord $revision, ParserOptions $options, array $usedOptions = null ): string { $usedOptions = ParserOptions::allCacheVaryingOptions(); $revId = $revision->getId(); $hash = $options->optionsHash( $usedOptions ); return $this->cache->makeKey( $this->name, $revId, $hash ); } /** * Retrieve the ParserOutput from cache. * false if not found or outdated. * * @param RevisionRecord $revision * @param ParserOptions $parserOptions * * @return ParserOutput|bool False on failure */ public function get( RevisionRecord $revision, ParserOptions $parserOptions ) { if ( $this->cacheExpiry <= 0 ) { // disabled return false; } if ( !$parserOptions->isSafeToCache() ) { $this->incrementStats( $revision, 'miss.unsafe' ); return false; } $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions ); $json = $this->cache->get( $cacheKey ); if ( $json === false ) { $this->incrementStats( $revision, 'miss.absent' ); return false; } $output = $this->restoreFromJson( $json, $cacheKey, ParserOutput::class ); if ( $output === null ) { $this->incrementStats( $revision, 'miss.unserialize' ); return false; } $cacheTime = (int)MWTimestamp::convert( TS_UNIX, $output->getCacheTime() ); $expiryTime = (int)MWTimestamp::convert( TS_UNIX, $this->cacheEpoch ); $expiryTime = max( $expiryTime, (int)MWTimestamp::now( TS_UNIX ) - $this->cacheExpiry ); if ( $cacheTime < $expiryTime ) { $this->incrementStats( $revision, 'miss.expired' ); return false; } $this->logger->debug( 'output cache hit' ); $this->incrementStats( $revision, 'hit' ); return $output; } /** * @param ParserOutput $output * @param RevisionRecord $revision * @param ParserOptions $parserOptions * @param string|null $cacheTime TS_MW timestamp when the output was generated */ public function save( ParserOutput $output, RevisionRecord $revision, ParserOptions $parserOptions, string $cacheTime = null ) { if ( !$output->hasText() ) { throw new InvalidArgumentException( 'Attempt to cache a ParserOutput with no text set!' ); } if ( $this->cacheExpiry <= 0 ) { // disabled return; } $cacheKey = $this->makeParserOutputKey( $revision, $parserOptions ); $output->setCacheTime( $cacheTime ?: wfTimestampNow() ); $output->setCacheRevisionId( $revision->getId() ); // Save the timestamp so that we don't have to load the revision row on view $output->setTimestamp( $revision->getTimestamp() ); $msg = "Saved in RevisionOutputCache with key $cacheKey" . " and timestamp $cacheTime" . " and revision id {$revision->getId()}."; $output->addCacheMessage( $msg ); // The ParserOutput might be dynamic and have been marked uncacheable by the parser. $output->updateCacheExpiry( $this->cacheExpiry ); $expiry = $output->getCacheExpiry(); if ( $expiry <= 0 ) { $this->incrementStats( $revision, 'save.uncacheable' ); return; } if ( !$parserOptions->isSafeToCache() ) { $this->incrementStats( $revision, 'save.unsafe' ); return; } $json = $this->encodeAsJson( $output, $cacheKey ); if ( $json === null ) { $this->incrementStats( $revision, 'save.nonserializable' ); return; } $this->cache->set( $cacheKey, $json, $expiry ); $this->incrementStats( $revision, 'save.success' ); } /** * @param string $jsonData * @param string $key * @param string $expectedClass * @return CacheTime|ParserOutput|null */ private function restoreFromJson( string $jsonData, string $key, string $expectedClass ) { try { /** @var CacheTime $obj */ $obj = $this->jsonCodec->unserialize( $jsonData, $expectedClass ); return $obj; } catch ( InvalidArgumentException $e ) { $this->logger->error( 'Unable to unserialize JSON', [ 'name' => $this->name, 'cache_key' => $key, 'message' => $e->getMessage() ] ); return null; } } /** * @param CacheTime $obj * @param string $key * @return string|null */ private function encodeAsJson( CacheTime $obj, string $key ) { try { return $this->jsonCodec->serialize( $obj ); } catch ( InvalidArgumentException $e ) { $this->logger->error( 'Unable to serialize JSON', [ 'name' => $this->name, 'cache_key' => $key, 'message' => $e->getMessage(), ] ); return null; } } }