primaryCache = $primaryCache; $this->secondaryCache = $secondaryCache; $this->revisionLookup = $revisionLookup; $this->revisionRenderer = $revisionRenderer; $this->statsDataFactory = $statsDataFactory; $this->lbFactory = $lbFactory; $this->loggerSpi = $loggerSpi; $this->wikiPageFactory = $wikiPageFactory; $this->titleFormatter = $titleFormatter; } /** * Use a cache? * * @param PageRecord $page * @param ParserOptions $parserOptions ParserOptions to check * @param RevisionRecord|null $rev * * @return string One of the CACHE_XXX constants. */ private function shouldUseCache( PageRecord $page, ParserOptions $parserOptions, ?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; } if ( !$rev || $rev->getId() === $page->getLatest( PageRecord::LOCAL ) ) { // current revision 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 { $useCache = $this->shouldUseCache( $page, $parserOptions, $revision ); if ( $useCache === self::CACHE_PRIMARY ) { $output = $this->primaryCache->get( $page, $parserOptions ); } elseif ( $useCache === self::CACHE_SECONDARY && $revision ) { $output = $this->secondaryCache->get( $revision, $parserOptions ); } else { $output = null; } $hitOrMiss = $output ? 'hit' : 'miss'; $this->statsDataFactory->increment( "ParserOutputAccess.Cache.$useCache.$hitOrMiss" ); 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 is 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; } $currentOrOld = ( $revision && $revision->getId() !== $page->getLatest() ) ? 'old' : 'current'; $this->statsDataFactory->increment( "ParserOutputAccess.Case.$currentOrOld" ); if ( !( $options & self::OPT_NO_CHECK_CACHE ) ) { $output = $this->getCachedParserOutput( $page, $parserOptions, $revision ); if ( $output ) { return Status::newGood( $output ); } } if ( !$revision ) { $revision = $page->getLatest() ? $this->revisionLookup->getRevisionById( $page->getLatest() ) : null; if ( !$revision ) { $this->statsDataFactory->increment( "ParserOutputAccess.Status.norev" ); return Status::newFatal( 'missing-revision', $page->getLatest() ); } } $work = $this->newPoolWorkArticleView( $page, $parserOptions, $revision, $options ); $work->execute(); $output = $work->getParserOutput(); $status = Status::newGood(); if ( $work->getError() ) { $status->merge( $work->getError() ); } if ( !$output && $status->isOK() ) { // TODO: PoolWorkArticle should properly report errors (T267610) $status->fatal( 'pool-errorunknown' ); } if ( $output && $status->isOK() ) { $status->setResult( true, $output ); } if ( $status->isOK() && $work->getIsDirty() ) { $staleReason = $work->getIsFastStale() ? 'view-pool-contention' : 'view-pool-overload'; $status->warning( 'view-pool-dirty-output' ); $status->warning( $staleReason ); } if ( $status->isGood() ) { $statusMsg = 'good'; } elseif ( $status->isOK() ) { $statusMsg = 'ok'; } else { $statusMsg = 'error'; } $this->statsDataFactory->increment( "ParserOutputAccess.Status.$statusMsg" ); 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 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 PoolWorkArticleView */ private function newPoolWorkArticleView( PageRecord $page, ParserOptions $parserOptions, RevisionRecord $revision, int $options ): PoolWorkArticleView { if ( $options & self::OPT_NO_UPDATE_CACHE ) { $useCache = self::CACHE_NONE; } else { $useCache = $this->shouldUseCache( $page, $parserOptions, $revision ); } switch ( $useCache ) { case self::CACHE_PRIMARY: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Current' ); $parserCacheMetadata = $this->primaryCache->getMetadata( $page ); $cacheKey = $this->primaryCache->makeParserOutputKey( $page, $parserOptions, $parserCacheMetadata ? $parserCacheMetadata->getUsedOptions() : null ); $workKey = $cacheKey . ':revid:' . $revision->getId(); return new PoolWorkArticleViewCurrent( $workKey, $page, $revision, $parserOptions, $this->revisionRenderer, $this->primaryCache, $this->lbFactory, $this->loggerSpi, $this->wikiPageFactory ); case $useCache == self::CACHE_SECONDARY: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Old' ); $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions ); return new PoolWorkArticleViewOld( $workKey, $this->secondaryCache, $revision, $parserOptions, $this->revisionRenderer, $this->loggerSpi ); default: $this->statsDataFactory->increment( 'ParserOutputAccess.PoolWork.Uncached' ); $workKey = $this->secondaryCache->makeParserOutputKey( $revision, $parserOptions ); return new PoolWorkArticleView( $workKey, $revision, $parserOptions, $this->revisionRenderer, $this->loggerSpi ); } // unreachable } }