MediaWiki  master
recountCategories.php
Go to the documentation of this file.
1 <?php
24 require_once __DIR__ . '/Maintenance.php';
25 
27 
40  private $mode;
41 
43  private $minimumId;
44 
45  public function __construct() {
46  parent::__construct();
47  $this->addDescription( <<<'TEXT'
48 This script refreshes the category membership counts stored in the category
49 table. As time passes, these counts often drift from the actual number of
50 category members. The script identifies rows where the value in the category
51 table does not match the number of categorylinks rows for that category, and
52 updates the category table accordingly.
53 
54 To fully refresh the data in the category table, you need to run this script
55 three times: once in each mode. Alternatively, just one mode can be run if
56 required.
57 TEXT
58  );
59  $this->addOption(
60  'mode',
61  '(REQUIRED) Which category count column to recompute: "pages", "subcats" or "files".',
62  true,
63  true
64  );
65  $this->addOption(
66  'begin',
67  'Only recount categories with cat_id greater than the given value',
68  false,
69  true
70  );
71  $this->addOption(
72  'throttle',
73  'Wait this many milliseconds after each batch. Default: 0',
74  false,
75  true
76  );
77 
78  $this->setBatchSize( 500 );
79  }
80 
81  public function execute() {
82  $this->mode = $this->getOption( 'mode' );
83  if ( !in_array( $this->mode, [ 'pages', 'subcats', 'files' ] ) ) {
84  $this->fatalError( 'Please specify a valid mode: one of "pages", "subcats" or "files".' );
85  }
86 
87  $this->minimumId = intval( $this->getOption( 'begin', 0 ) );
88 
89  // do the work, batch by batch
90  $affectedRows = 0;
91  while ( ( $result = $this->doWork() ) !== false ) {
92  $affectedRows += $result;
93  usleep( $this->getOption( 'throttle', 0 ) * 1000 );
94  }
95 
96  $this->output( "Done! Updated the {$this->mode} counts of $affectedRows categories.\n" .
97  "Now run the script using the other --mode options if you haven't already.\n" );
98  if ( $this->mode === 'pages' ) {
99  $this->output(
100  "Also run 'php cleanupEmptyCategories.php --mode remove' to remove empty,\n" .
101  "nonexistent categories from the category table.\n\n" );
102  }
103  }
104 
105  protected function doWork() {
106  $this->output( "Finding up to {$this->getBatchSize()} drifted rows " .
107  "greater than cat_id {$this->minimumId}...\n" );
108 
109  $countingConds = [ 'cl_to = cat_title' ];
110  if ( $this->mode === 'subcats' ) {
111  $countingConds['cl_type'] = 'subcat';
112  } elseif ( $this->mode === 'files' ) {
113  $countingConds['cl_type'] = 'file';
114  }
115 
116  $dbr = $this->getDB( DB_REPLICA, 'vslow' );
117  $countingSubquery = $dbr->selectSQLText( 'categorylinks',
118  'COUNT(*)',
119  $countingConds,
120  __METHOD__ );
121 
122  // First, let's find out which categories have drifted and need to be updated.
123  // The query counts the categorylinks for each category on the replica DB,
124  // but this data can't be used for updating the master, so we don't include it
125  // in the results.
126  $idsToUpdate = $dbr->selectFieldValues( 'category',
127  'cat_id',
128  [
129  'cat_id > ' . $this->minimumId,
130  "cat_{$this->mode} != ($countingSubquery)"
131  ],
132  __METHOD__,
133  [ 'LIMIT' => $this->getBatchSize() ]
134  );
135  if ( !$idsToUpdate ) {
136  return false;
137  }
138  $this->output( "Updating cat_{$this->mode} field on " .
139  count( $idsToUpdate ) . " rows...\n" );
140 
141  // In the next batch, start where this query left off. The rows selected
142  // in this iteration shouldn't be selected again after being updated, but
143  // we still keep track of where we are up to, as extra protection against
144  // infinite loops.
145  $this->minimumId = end( $idsToUpdate );
146 
147  // Now, on master, find the correct counts for these categories.
148  $dbw = $this->getDB( DB_MASTER );
149  $res = $dbw->select( 'category',
150  [ 'cat_id', 'count' => "($countingSubquery)" ],
151  [ 'cat_id' => $idsToUpdate ],
152  __METHOD__ );
153 
154  // Update the category counts on the rows we just identified.
155  // This logic is equivalent to Category::refreshCounts, except here, we
156  // don't remove rows when cat_pages is zero and the category description page
157  // doesn't exist - instead we print a suggestion to run
158  // cleanupEmptyCategories.php.
159  $affectedRows = 0;
160  foreach ( $res as $row ) {
161  $dbw->update( 'category',
162  [ "cat_{$this->mode}" => $row->count ],
163  [
164  'cat_id' => $row->cat_id,
165  "cat_{$this->mode} != " . (int)( $row->count ),
166  ],
167  __METHOD__ );
168  $affectedRows += $dbw->affectedRows();
169  }
170 
171  MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->waitForReplication();
172 
173  return $affectedRows;
174  }
175 }
176 
177 $maintClass = RecountCategories::class;
178 require_once RUN_MAINTENANCE_IF_MAIN;
RUN_MAINTENANCE_IF_MAIN
const RUN_MAINTENANCE_IF_MAIN
Definition: Maintenance.php:38
RecountCategories\__construct
__construct()
Default constructor.
Definition: recountCategories.php:45
RecountCategories
Maintenance script that refreshes category membership counts in the category table.
Definition: recountCategories.php:38
RecountCategories\$minimumId
int $minimumId
Definition: recountCategories.php:43
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:162
Maintenance\fatalError
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Definition: Maintenance.php:487
Maintenance\addDescription
addDescription( $text)
Set the description text.
Definition: Maintenance.php:327
Maintenance
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: Maintenance.php:55
$res
$res
Definition: testCompression.php:57
RecountCategories\execute
execute()
Do the actual work.
Definition: recountCategories.php:81
$dbr
$dbr
Definition: testCompression.php:54
Maintenance\addOption
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
Definition: Maintenance.php:245
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
DB_MASTER
const DB_MASTER
Definition: defines.php:26
Maintenance\getDB
getDB( $db, $groups=[], $dbDomain=false)
Returns a database to be used by current maintenance script.
Definition: Maintenance.php:1370
Maintenance\getOption
getOption( $name, $default=null)
Get an option, or return the default.
Definition: Maintenance.php:281
Maintenance\getBatchSize
getBatchSize()
Returns batch size.
Definition: Maintenance.php:366
Maintenance\output
output( $out, $channel=null)
Throw some output to the user.
Definition: Maintenance.php:434
RecountCategories\doWork
doWork()
Definition: recountCategories.php:105
$maintClass
$maintClass
Definition: recountCategories.php:177
RecountCategories\$mode
string $mode
Definition: recountCategories.php:40
Maintenance\setBatchSize
setBatchSize( $s=0)
Set the batch size.
Definition: Maintenance.php:374