49 private const ENQUEUE_FUDGE_SEC = 60;
58 'categoryMembershipChange',
60 'pageId' => $page->
getId(),
61 'revTimestamp' => $revisionTimestamp,
64 'removeDuplicates' =>
true,
65 'removeDuplicatesIgnoreParams' => [
'revTimestamp' ]
78 parent::__construct(
'categoryMembershipChange', $page,
$params );
81 $this->removeDuplicates =
true;
84 public function run() {
85 $services = MediaWikiServices::getInstance();
86 $lbFactory = $services->getDBLoadBalancerFactory();
87 $lb = $lbFactory->getMainLB();
90 $this->ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
92 $page = $services->getWikiPageFactory()->newFromID( $this->params[
'pageId'], WikiPage::READ_LATEST );
94 $this->
setLastError(
"Could not find page #{$this->params['pageId']}" );
100 if ( !$lb->waitForPrimaryPos( $dbr ) ) {
101 $this->
setLastError(
"Timed out while pre-waiting for replica DB to catch up" );
106 $lockKey =
"{$dbw->getDomainID()}:CategoryMembershipChange:{$page->getId()}";
107 $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
108 if ( !$scopedLock ) {
109 $this->
setLastError(
"Could not acquire lock '$lockKey'" );
114 if ( !$lb->waitForPrimaryPos( $dbr ) ) {
115 $this->
setLastError(
"Timed out while waiting for replica DB to catch up" );
119 $dbr->flushSnapshot( __METHOD__ );
121 $cutoffUnix =
wfTimestamp( TS_UNIX, $this->params[
'revTimestamp'] );
124 $cutoffUnix -= self::ENQUEUE_FUDGE_SEC;
128 $subQuery = $dbr->newSelectQueryBuilder()
130 ->from(
'recentchanges' )
131 ->where(
'rc_this_oldid = rev_id' )
133 $row = $dbr->newSelectQueryBuilder()
134 ->select( [
'rev_timestamp',
'rev_id' ] )
136 ->where( [
'rev_page' => $page->
getId() ] )
137 ->andWhere( $dbr->buildComparison(
'>=', [
'rev_timestamp' => $dbr->timestamp( $cutoffUnix ) ] ) )
138 ->andWhere(
'EXISTS (' . $subQuery->caller( __METHOD__ )->getSQL() .
')' )
139 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC )
140 ->caller( __METHOD__ )->fetchRow();
144 $cutoffUnix =
wfTimestamp( TS_UNIX, $row->rev_timestamp );
145 $lastRevId = (int)$row->rev_id;
152 $revisionStore = $services->getRevisionStore();
153 $res = $revisionStore->newSelectQueryBuilder( $dbr )
156 'rev_page' => $page->
getId(),
157 $dbr->buildComparison(
'>', [
158 'rev_timestamp' => $dbr->timestamp( $cutoffUnix ),
159 'rev_id' => $lastRevId,
162 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_ASC )
163 ->caller( __METHOD__ )->fetchResultSet();
166 foreach ( $res as $row ) {
185 if ( $newRev->
isDeleted( RevisionRecord::DELETED_TEXT ) ) {
189 $services = MediaWikiServices::getInstance();
192 $oldRev = $services->getRevisionLookup()
193 ->getRevisionById( $newRev->
getParentId(), RevisionLookup::READ_LATEST );
194 if ( !$oldRev || $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
202 $categoryChanges = $this->getExplicitCategoriesChanges( $page, $newRev, $oldRev );
203 [ $categoryInserts, $categoryDeletes ] = $categoryChanges;
204 if ( !$categoryInserts && !$categoryDeletes ) {
208 $blc = $services->getBacklinkCacheFactory()->getBacklinkCache(
$title );
210 $catMembChange->checkTemplateLinks();
212 $batchSize = $services->getMainConfig()->get( MainConfigNames::UpdateRowsPerQuery );
215 foreach ( $categoryInserts as $categoryName ) {
216 $categoryTitle = Title::makeTitle(
NS_CATEGORY, $categoryName );
217 $catMembChange->triggerCategoryAddedNotification( $categoryTitle );
218 if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) {
223 foreach ( $categoryDeletes as $categoryName ) {
224 $categoryTitle = Title::makeTitle(
NS_CATEGORY, $categoryName );
225 $catMembChange->triggerCategoryRemovedNotification( $categoryTitle );
226 if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) {
232 private function getExplicitCategoriesChanges(
242 $oldCategories = $oldRev
243 ? $this->getCategoriesAtRev( $page, $oldRev, $parseTimestamp )
246 $newCategories = $this->getCategoriesAtRev( $page, $newRev, $parseTimestamp );
248 $categoryInserts = array_values( array_diff( $newCategories, $oldCategories ) );
249 $categoryDeletes = array_values( array_diff( $oldCategories, $newCategories ) );
251 return [ $categoryInserts, $categoryDeletes ];
262 $services = MediaWikiServices::getInstance();
264 $options->setTimestamp( $parseTimestamp );
265 $options->setRenderReason(
'CategoryMembershipChangeJob' );
268 ? $services->getParserCache()->get( $page, $options )
271 if ( !$output || $output->getCacheRevisionId() !== $rev->
getId() ) {
272 $output = $services->getRevisionRenderer()->getRenderedRevision( $rev, $options )
273 ->getRevisionParserOutput();
279 return $output->getCategoryNames();
283 $info = parent::getDeduplicationInfo();
284 unset( $info[
'params'][
'revTimestamp'] );
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Job to add recent change entries mentioning category membership changes.
static newSpec(PageIdentity $page, $revisionTimestamp)
__construct(PageIdentity $page, array $params)
Constructor for use by the Job Queue infrastructure.
getDeduplicationInfo()
Subclasses may need to override this to make duplication detection work.
notifyUpdatesForRevision(LBFactory $lbFactory, WikiPage $page, RevisionRecord $newRev)
Job queue task description base code.
Class to both describe a background job and handle jobs.
array $params
Array of job parameters.
A class containing constants representing the names of configuration variables.
Base representation for an editable wiki page.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
getId( $wikiId=self::LOCAL)
getTitle()
Get the title object of the article.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.