MediaWiki master
CategoryCountUpdateJob.php
Go to the documentation of this file.
1<?php
8
17
32 private IConnectionProvider $connectionProvider;
33 private NamespaceInfo $namespaceInfo;
34
42 public static function newSpec( PageIdentity $page, array $insertedLinks, array $deletedLinks, int $batchSize ) {
43 return new JobSpecification(
44 'CategoryCountUpdateJob',
45 [
46 'pageId' => $page->getId(),
47 'namespace' => $page->getNamespace(),
48 'insertedLinks' => $insertedLinks,
49 'deletedLinks' => $deletedLinks,
50 'batchSize' => $batchSize,
51 ],
52 [],
53 $page
54 );
55 }
56
63 public function __construct(
64 PageIdentity $page,
65 array $params,
66 IConnectionProvider $connectionProvider,
67 NamespaceInfo $namespaceInfo
68 ) {
69 parent::__construct( 'CategoryCountUpdateJob', $page, $params );
70
71 $this->connectionProvider = $connectionProvider;
72 $this->namespaceInfo = $namespaceInfo;
73 }
74
76 public function run() {
77 $insertedLinks = $this->params['insertedLinks'];
78 $deletedLinks = $this->params['deletedLinks'];
79
80 if ( !$insertedLinks && !$deletedLinks ) {
81 return true;
82 }
83
84 $ticket = $this->connectionProvider->getEmptyTransactionTicket( __METHOD__ );
85 $size = $this->params['batchSize'] ?? 100;
86
87 // T163801: try to release any row locks to reduce contention
88 $this->connectionProvider->commitAndWaitForReplication( __METHOD__, $ticket );
89 if ( count( $insertedLinks ) + count( $deletedLinks ) < $size ) {
90 $this->updateCategoryCounts( $insertedLinks, $deletedLinks );
91 $this->connectionProvider->commitAndWaitForReplication( __METHOD__, $ticket );
92 } else {
93 $addedChunks = array_chunk( $insertedLinks, $size );
94 foreach ( $addedChunks as $chunk ) {
95 $this->updateCategoryCounts( $chunk, [] );
96 if ( count( $addedChunks ) > 1 ) {
97 $this->connectionProvider->commitAndWaitForReplication( __METHOD__, $ticket );
98 }
99 }
100 $deletedChunks = array_chunk( $deletedLinks, $size );
101 foreach ( $deletedChunks as $chunk ) {
102 $this->updateCategoryCounts( [], $chunk );
103 if ( count( $deletedChunks ) > 1 ) {
104 $this->connectionProvider->commitAndWaitForReplication( __METHOD__, $ticket );
105 }
106 }
107 }
108
109 return true;
110 }
111
112 private function updateCategoryCounts( array $added, array $deleted ) {
113 $id = $this->params['pageId'];
114
115 // Guard against data corruption T301433
116 $added = array_map( 'strval', $added );
117 $deleted = array_map( 'strval', $deleted );
118 $type = $this->namespaceInfo->getCategoryLinkType( $this->params['namespace'] );
119
120 $addFields = [ 'cat_pages' => new RawSQLValue( 'cat_pages + 1' ) ];
121 $removeFields = [ 'cat_pages' => new RawSQLValue( 'cat_pages - 1' ) ];
122 if ( $type !== 'page' ) {
123 $addFields["cat_{$type}s"] = new RawSQLValue( "cat_{$type}s + 1" );
124 $removeFields["cat_{$type}s"] = new RawSQLValue( "cat_{$type}s - 1" );
125 }
126
127 $dbw = $this->connectionProvider->getPrimaryDatabase();
128 $res = $dbw->newSelectQueryBuilder()
129 ->select( [ 'cat_id', 'cat_title' ] )
130 ->from( 'category' )
131 ->where( [ 'cat_title' => array_merge( $added, $deleted ) ] )
132 ->caller( __METHOD__ )
133 ->fetchResultSet();
134 $existingCategories = [];
135 foreach ( $res as $row ) {
136 $existingCategories[$row->cat_id] = $row->cat_title;
137 }
138 $existingAdded = array_intersect( $existingCategories, $added );
139 $existingDeleted = array_intersect( $existingCategories, $deleted );
140 $missingAdded = array_diff( $added, $existingAdded );
141
142 // For category rows that already exist, do a plain
143 // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
144 // to avoid creating gaps in the cat_id sequence.
145 if ( $existingAdded ) {
146 $dbw->newUpdateQueryBuilder()
147 ->update( 'category' )
148 ->set( $addFields )
149 ->where( [ 'cat_id' => array_keys( $existingAdded ) ] )
150 ->caller( __METHOD__ )->execute();
151 }
152
153 if ( $missingAdded ) {
154 $queryBuilder = $dbw->newInsertQueryBuilder()
155 ->insertInto( 'category' )
156 ->onDuplicateKeyUpdate()
157 ->uniqueIndexFields( [ 'cat_title' ] )
158 ->set( $addFields );
159 foreach ( $missingAdded as $cat ) {
160 $queryBuilder->row( [
161 'cat_title' => $cat,
162 'cat_pages' => 1,
163 'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
164 'cat_files' => ( $type === 'file' ) ? 1 : 0,
165 ] );
166 }
167 $queryBuilder->caller( __METHOD__ )->execute();
168 }
169
170 if ( $existingDeleted ) {
171 $dbw->newUpdateQueryBuilder()
172 ->update( 'category' )
173 ->set( $removeFields )
174 ->where( [ 'cat_id' => array_keys( $existingDeleted ) ] )
175 ->caller( __METHOD__ )->execute();
176 }
177
178 foreach ( $deleted as $catName ) {
179 $cat = Category::newFromName( $catName );
180 // Refresh counts on categories that should be empty now (after commit, T166757)
181 DeferredUpdates::addCallableUpdate( static function () use ( $cat ) {
182 $cat->refreshCountsIfEmpty();
183 } );
184 }
185 }
186}
Category objects are immutable, strictly speaking.
Definition Category.php:29
Defer callable updates to run later in the PHP process.
Job queue task description base code.
Describe and execute a background job.
Definition Job.php:28
array $params
Array of job parameters.
Definition Job.php:33
Job to update category membership counts.
__construct(PageIdentity $page, array $params, IConnectionProvider $connectionProvider, NamespaceInfo $namespaceInfo)
Constructor for use by the Job Queue infrastructure.
static newSpec(PageIdentity $page, array $insertedLinks, array $deletedLinks, int $batchSize)
run()
Run the job.If this method returns false or completes exceptionally, the job runner will retry execut...
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Raw SQL value to be used in query builders.
Interface for objects (potentially) representing an editable wiki page.
getId( $wikiId=self::LOCAL)
Returns the page ID.
getNamespace()
Returns the page's namespace number.
Provide primary and replica IDatabase connections.