parserCacheFactory = $parserCacheFactory; $this->revisionLookup = $revisionLookup; $this->revisionRenderer = $revisionRenderer; $this->statsDataFactory = $statsDataFactory; $this->lbFactory = $lbFactory; $this->chronologyProtector = $chronologyProtector; $this->loggerSpi = $loggerSpi; $this->wikiPageFactory = $wikiPageFactory; $this->titleFormatter = $titleFormatter; } /** * Use a cache? * * @param PageRecord $page * @param RevisionRecord|null $rev * * @return string One of the CACHE_XXX constants. */ private function shouldUseCache( PageRecord $page, ?RevisionRecord $rev ) { if ( $rev && !$rev->getId() ) { // The revision isn't from the database, so the output can't safely be cached. return self::CACHE_NONE; } // NOTE: Keep in sync with ParserWikiPage::shouldCheckParserCache(). // NOTE: when we allow caching of old revisions in the future, // we must not allow caching of deleted revisions. $wikiPage = $this->wikiPageFactory->newFromTitle( $page ); if ( !$page->exists() || !$wikiPage->getContentHandler()->isParserCacheSupported() ) { return self::CACHE_NONE; } $isOld = $rev && $rev->getId() !== $page->getLatest(); if ( !$isOld ) { return self::CACHE_PRIMARY; } if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) { // deleted/suppressed revision return self::CACHE_NONE; } return self::CACHE_SECONDARY; } /** * Returns the rendered output for the given page if it is present in the cache. * * @param PageRecord $page * @param ParserOptions $parserOptions * @param RevisionRecord|null $revision * @param int $options Bitfield using the OPT_XXX constants * * @return ParserOutput|null */ public function getCachedParserOutput( PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision = null, int $options = 0 ): ?ParserOutput { $isOld = $revision && $revision->getId() !== $page->getLatest(); $useCache = $this->shouldUseCache( $page, $revision ); $primaryCache = $this->getPrimaryCache( $parserOptions ); $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions ); if ( $useCache === self::CACHE_PRIMARY ) { if ( isset( $this->localCache[$classCacheKey] ) && !$isOld ) { return $this->localCache[$classCacheKey]; } $output = $primaryCache->get( $page, $parserOptions ); } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) { $secondaryCache = $this->getSecondaryCache( $parserOptions ); $output = $secondaryCache->get( $revision, $parserOptions ); } else { $output = null; } if ( $output && !$isOld ) { $this->localCache[$classCacheKey] = $output; } if ( $output ) { $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.hit" ); } else { $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.miss" ); } return $output ?: null; // convert false to null } /** * Returns the rendered output for the given page. * Caching and concurrency control is applied. * * @param PageRecord $page * @param ParserOptions $parserOptions * @param RevisionRecord|null $revision * @param int $options Bitfield using the OPT_XXX constants * * @return Status containing a ParserOutput if no error occurred. * Well known errors and warnings include the following messages: * - 'view-pool-dirty-output' (warning) The output is dirty (from a stale cache entry). * - 'view-pool-contention' (warning) Dirty output was returned immediately instead of * waiting to acquire a work lock (when "fast stale" mode is enabled in PoolCounter). * - 'view-pool-timeout' (warning) Dirty output was returned after failing to acquire * a work lock (got QUEUE_FULL or TIMEOUT from PoolCounter). * - 'pool-queuefull' (error) unable to acquire work lock, and no cached content found. * - 'pool-timeout' (error) unable to acquire work lock, and no cached content found. * - 'pool-servererror' (error) PoolCounterWork failed due to a lock service error. * - 'pool-unknownerror' (error) PoolCounterWork failed for an unknown reason. * - 'nopagetext' (error) The page does not exist */ public function getParserOutput( PageRecord $page, ParserOptions $parserOptions, ?RevisionRecord $revision = null, int $options = 0 ): Status { $error = $this->checkPreconditions( $page, $revision, $options ); if ( $error ) { $this->statsDataFactory->increment( "ParserOutputAccess.Case.error" ); return $error; } $isOld = $revision && $revision->getId() !== $page->getLatest(); if ( $isOld ) { $this->statsDataFactory->increment( 'ParserOutputAccess.Case.old' ); } else { $this->statsDataFactory->increment( 'ParserOutputAccess.Case.current' ); } if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) { $output = $this->getCachedParserOutput( $page, $parserOptions, $revision ); if ( $output ) { return Status::newGood( $output ); } } if ( !$revision ) { $revId = $page->getLatest(); $revision = $revId ? $this->revisionLookup->getRevisionById( $revId ) : null; if ( !$revision ) { $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" ); return Status::newFatal( 'missing-revision', $revId ); } } $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options ); /** @var Status $status */ $status = $work->execute(); $output = $status->getValue(); Assert::postcondition( $output || !$status->isOK(), 'Worker returned invalid status' ); if ( $output && !$isOld ) { $primaryCache = $this->getPrimaryCache( $parserOptions ); $classCacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions ); $this->localCache[$classCacheKey] = $output; } if ( $status->isGood() ) { $this->statsDataFactory->increment( 'ParserOutputAccess.Status.good' ); } elseif ( $status->isOK() ) { $this->statsDataFactory->increment( 'ParserOutputAccess.Status.ok' ); } else { $this->statsDataFactory->increment( 'ParserOutputAccess.Status.error' ); } return $status; } /** * @param PageRecord $page * @param RevisionRecord|null $revision * @param int $options * * @return Status|null */ private function checkPreconditions( PageRecord $page, ?RevisionRecord $revision = null, int $options = 0 ): ?Status { if ( !$page->exists() ) { return Status::newFatal( 'nopagetext' ); } if ( !( $options & self::OPT_NO_UPDATE_CACHE ) && $revision && !$revision->getId() ) { throw new InvalidArgumentException( 'The revision does not have a known ID. Use OPT_NO_CACHE.' ); } if ( $revision && $revision->getPageId() !== $page->getId() ) { throw new InvalidArgumentException( 'The revision does not belong to the given page.' ); } if ( $revision && !( $options & self::OPT_NO_AUDIENCE_CHECK ) ) { // NOTE: If per-user checks are desired, the caller should perform them and // then set OPT_NO_AUDIENCE_CHECK if they passed. if ( !$revision->audienceCan( RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC ) ) { return Status::newFatal( 'missing-revision-permission', $revision->getId(), $revision->getTimestamp(), $this->titleFormatter->getPrefixedDBkey( $page ) ); } } return null; } /** * @param PageRecord $page * @param ParserOptions $parserOptions * @param RevisionRecord $revision * @param int $options * * @return PoolCounterWork */ private function newPoolWorkArticleView( PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, int $options ): PoolCounterWork { $useCache = $this->shouldUseCache( $page, $revision ); switch ( $useCache ) { case self::CACHE_PRIMARY: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' ); $primaryCache = $this->getPrimaryCache( $parserOptions ); $parserCacheMetadata = $primaryCache->getMetadata( $page ); $cacheKey = $primaryCache->makeParserOutputKey( $page, $parserOptions, $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null ); $workKey = $cacheKey . ':revid:' . $revision->getId(); return new PoolWorkArticleViewCurrent( $workKey, $page, $revision, $parserOptions, $this->revisionRenderer, $primaryCache, $this->lbFactory, $this->chronologyProtector, $this->loggerSpi, $this->wikiPageFactory, !( $options & self::OPT_NO_UPDATE_CACHE ), (bool)( $options & self::OPT_LINKS_UPDATE ) ); case self::CACHE_SECONDARY: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' ); $secondaryCache = $this->getSecondaryCache( $parserOptions ); $workKey = $secondaryCache->makeParserOutputKey( $revision, $parserOptions ); return new PoolWorkArticleViewOld( $workKey, $secondaryCache, $revision, $parserOptions, $this->revisionRenderer, $this->loggerSpi ); default: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' ); $secondaryCache = $this->getSecondaryCache( $parserOptions ); $workKey = $secondaryCache->makeParserOutputKeyOptionalRevId( $revision, $parserOptions ); return new PoolWorkArticleView( $workKey, $revision, $parserOptions, $this->revisionRenderer, $this->loggerSpi ); } // unreachable } private function getPrimaryCache( ParserOptions $pOpts ): ParserCache { if ( $pOpts->getUseParsoid() ) { // T331148: This is different from // ParsoidOutputAccess::PARSOID_PARSER_CACHE_NAME; will be // renamed once the contents cached on the read-views and // the REST path are identical. return $this->parserCacheFactory->getParserCache( 'parsoid-' . ParserCacheFactory::DEFAULT_NAME ); } return $this->parserCacheFactory->getParserCache( ParserCacheFactory::DEFAULT_NAME ); } private function getSecondaryCache( ParserOptions $pOpts ): RevisionOutputCache { if ( $pOpts->getUseParsoid() ) { return $this->parserCacheFactory->getRevisionOutputCache( 'parsoid-' . ParserCacheFactory::DEFAULT_RCACHE_NAME ); } return $this->parserCacheFactory->getRevisionOutputCache( ParserCacheFactory::DEFAULT_RCACHE_NAME ); } }