107 private const NORMAL_MAX_LAG = 10;
109 private const LAG_WAIT_TIMEOUT = 15;
115 'The given PageIdentity {pageIdentity} does not represent a proper page',
116 [
'pageIdentity' => $page ]
120 parent::__construct(
'refreshLinks', $page,
$params );
122 $this->removeDuplicates = (
128 $this->params += [
'causeAction' =>
'RefreshLinksJob',
'causeAgent' =>
'unknown' ];
132 $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
142 $job->command =
'refreshLinksPrioritized';
154 $job->command =
'refreshLinksDynamic';
162 if ( !empty( $this->params[
'recursive'] ) ) {
168 $services = MediaWikiServices::getInstance();
169 if ( !isset( $this->params[
'range'] ) ) {
170 $lbFactory = $services->getDBLoadBalancerFactory();
171 if ( !$lbFactory->waitForReplication( [
172 'timeout' => self::LAG_WAIT_TIMEOUT
175 $stats = $services->getStatsFactory();
176 $stats->
getCounter(
'refreshlinks_warnings_total' )
177 ->setLabel(
'reason',
'lag_wait_failed' )
178 ->copyToStatsdAt(
'refreshlinks_warning.lag_wait_failed' )
184 $extraParams[
'triggeredRecursive'] =
true;
186 $extraParams[
'causeAction'] = $this->params[
'causeAction'];
187 $extraParams[
'causeAgent'] = $this->params[
'causeAgent'];
192 $services->getMainConfig()->get( MainConfigNames::UpdateRowsPerJob ),
194 [
'params' => $extraParams ]
196 $services->getJobQueueGroup()->push( $jobs );
198 } elseif ( isset( $this->params[
'pages'] ) ) {
200 foreach ( $this->params[
'pages'] as [ $ns, $dbKey ] ) {
201 $title = Title::makeTitleSafe( $ns, $dbKey );
223 $services = MediaWikiServices::getInstance();
224 $stats = $services->getStatsFactory();
225 $renderer = $services->getRevisionRenderer();
226 $parserCache = $services->getParserCache();
227 $lbFactory = $services->getDBLoadBalancerFactory();
228 $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
231 $page = $services->getWikiPageFactory()->newFromTitle( $pageIdentity );
236 $logger = LoggerFactory::getInstance(
'RefreshLinksJob' );
238 'The page does not exist. Perhaps it was deleted?',
240 'page_title' => $this->title->getPrefixedDBkey(),
241 'job_params' => $this->getParams(),
242 'job_metadata' => $this->getMetadata()
245 $this->incrementFailureCounter( $stats,
'page_not_found' );
255 $dbw = $lbFactory->getPrimaryDatabase();
257 $scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->
getId(),
'job' );
258 if ( $scopedLock ===
null ) {
260 $this->
setLastError(
'LinksUpdate already running for this page, try again later.' );
261 $this->incrementFailureCounter( $stats,
'lock_failure' );
267 if ( $this->isAlreadyRefreshed( $page ) ) {
270 $stats->
getCounter(
'refreshlinks_superseded_updates_total' )
271 ->copyToStatsdAt(
'refreshlinks_outcome.good_update_superseded' )
279 $lbFactory->flushReplicaSnapshots( __METHOD__ );
281 $lbFactory->beginPrimaryChanges( __METHOD__ );
282 $output = $this->getParserOutput( $renderer, $parserCache, $page, $stats );
283 $options = $this->getDataUpdateOptions();
284 $lbFactory->commitPrimaryChanges( __METHOD__ );
294 $options[
'known-revision-output'] = $output;
297 InfoAction::invalidateCache( $page );
307 $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
315 private function getLagAwareRootTimestamp() {
317 $rootTimestamp = $this->params[
'rootJobTimestamp'] ??
null;
318 if ( $rootTimestamp ===
null ) {
322 if ( !empty( $this->params[
'isOpportunistic'] ) ) {
325 $lagAwareTimestamp = $rootTimestamp;
330 (
int)
wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
334 return $lagAwareTimestamp;
341 private function isAlreadyRefreshed(
WikiPage $page ) {
342 $lagAwareTimestamp = $this->getLagAwareRootTimestamp();
344 return ( $lagAwareTimestamp !==
null && $page->
getLinksTimestamp() > $lagAwareTimestamp );
352 private function shouldGenerateHTMLOnEdit(
RevisionRecord $revision ): bool {
354 foreach ( $revision->
getSlots()->getSlotRoles() as $role ) {
355 $slot = $revision->
getSlots()->getSlot( $role );
356 $contentHandler = $services->getContentHandlerFactory()->getContentHandler( $slot->getModel() );
357 if ( $contentHandler->generateHTMLOnEdit() ) {
373 private function getParserOutput(
379 $revision = $this->getCurrentRevisionIfUnchanged( $page, $stats );
385 $cachedOutput = $this->getParserOutputFromCache( $parserCache, $page, $revision, $stats );
386 if ( $cachedOutput ) {
387 return $cachedOutput;
390 $causeAction = $this->params[
'causeAction'] ??
'RefreshLinksJob';
395 [
'audience' => $revision::RAW,
'causeAction' => $causeAction ]
399 $output = $renderedRevision->getRevisionParserOutput( [
401 'generate-html' => $this->shouldGenerateHTMLOnEdit( $revision )
403 $output->setCacheTime( $parseTimestamp );
415 private function getCurrentRevisionIfUnchanged(
423 $latest = $title->getLatestRevID( IDBAccessObject::READ_LATEST );
425 $triggeringRevisionId = $this->params[
'triggeringRevisionId'] ??
null;
426 if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) {
428 $this->incrementFailureCounter( $stats,
'rev_not_current' );
429 $this->
setLastError(
"Revision $triggeringRevisionId is not current" );
438 $this->incrementFailureCounter( $stats,
'rev_not_found' );
439 $this->
setLastError(
"Revision not found for {$title->getPrefixedDBkey()}" );
447 $this->incrementFailureCounter( $stats,
'rev_not_current' );
448 $this->
setLastError(
"Revision {$revision->getId()} is not current" );
465 private function getParserOutputFromCache(
471 $cachedOutput =
null;
475 $rootTimestamp = $this->params[
'rootJobTimestamp'] ??
null;
476 if ( $rootTimestamp !==
null ) {
477 $opportunistic = !empty( $this->params[
'isOpportunistic'] );
478 if ( $page->
getTouched() >= $rootTimestamp || $opportunistic ) {
483 $output = $parserCache->
getDirty( $page, $parserOptions );
486 $output->getCacheRevisionId() == $currentRevision->
getId() &&
487 $output->getCacheTime() >= $this->getLagAwareRootTimestamp()
489 $cachedOutput = $output;
494 if ( $cachedOutput ) {
495 $stats->
getCounter(
'refreshlinks_parsercache_operations_total' )
496 ->setLabel(
'status',
'cache_hit' )
497 ->copyToStatsdAt(
'refreshlinks.parser_cached' )
500 $stats->
getCounter(
'refreshlinks_parsercache_operations_total' )
501 ->setLabel(
'status',
'cache_miss' )
502 ->copyToStatsdAt(
'refreshlinks.parser_uncached' )
506 return $cachedOutput;
516 private function incrementFailureCounter(
StatsFactory $stats, $reason ): void {
517 $stats->getCounter(
'refreshlinks_failures_total' )
518 ->setLabel(
'reason', $reason )
519 ->copyToStatsdAt(
"refreshlinks_outcome.bad_$reason" )
526 private function getDataUpdateOptions() {
528 'recursive' => !empty( $this->params[
'useRecursiveLinksUpdate'] ),
530 'causeAction' => $this->params[
'causeAction'],
531 'causeAgent' => $this->params[
'causeAgent']
533 if ( !empty( $this->params[
'triggeringUser'] ) ) {
534 $userInfo = $this->params[
'triggeringUser'];
535 if ( $userInfo[
'userId'] ) {
536 $options[
'triggeringUser'] = User::newFromId( $userInfo[
'userId'] );
539 $options[
'triggeringUser'] = User::newFromName( $userInfo[
'userName'],
false );
547 $info = parent::getDeduplicationInfo();
548 unset( $info[
'causeAction'] );
549 unset( $info[
'causeAgent'] );
550 if ( is_array( $info[
'params'] ) ) {
553 if ( isset( $info[
'params'][
'pages'] ) ) {
554 unset( $info[
'namespace'] );
555 unset( $info[
'title'] );
563 if ( !empty( $this->params[
'recursive'] ) ) {
565 } elseif ( isset( $this->params[
'pages'] ) ) {
566 return count( $this->params[
'pages'] );
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
array $params
The job parameters.
setLastError( $error)
This is actually implemented in the Job class.
static partitionBacklinkJob(Job $job, $bSize, $cSize, $opts=[])
Break down $job into approximately ($bSize/$cSize) leaf jobs and a single partition job that covers t...
Describe and execute a background job.
A class containing constants representing the names of configuration variables.
Exception if a PageIdentity is an invalid argument.
Cache for ParserOutput objects corresponding to the latest page revisions.
getDirty(PageRecord $page, $popts)
Retrieve the ParserOutput from ParserCache, even if it's outdated.
Job to update link tables for rerendered wiki pages.
getDeduplicationInfo()
Subclasses may need to override this to make duplication detection work.
runForTitle(PageIdentity $pageIdentity)
static newDynamic(PageIdentity $page, array $params)
static newPrioritized(PageIdentity $page, array $params)
__construct(PageIdentity $page, array $params)
Base representation for an editable wiki page.
getLinksTimestamp()
Get the page_links_updated field.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getId( $wikiId=self::LOCAL)
getTitle()
Get the title object of the article.
doSecondaryDataUpdates(array $options=[])
Do secondary data updates (such as updating link tables).
loadPageData( $from='fromdb')
Load the object from a given source by title.
getRevisionRecord()
Get the latest revision.
getTouched()
Get the page_touched field.
Interface for objects (potentially) representing an editable wiki page.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
if(count( $args)< 1) $job