Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PruneFRIncludeData
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 3
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pruneFlaggedRevs
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
272
1<?php
2/**
3 * @ingroup Maintenance
4 */
5
6if ( getenv( 'MW_INSTALL_PATH' ) ) {
7    $IP = getenv( 'MW_INSTALL_PATH' );
8} else {
9    $IP = __DIR__ . '/../../..';
10}
11
12require_once "$IP/maintenance/Maintenance.php";
13
14class PruneFRIncludeData extends Maintenance {
15
16    public function __construct() {
17        parent::__construct();
18        $this->addDescription( "This script clears template data for reviewed versions" .
19            "that are 1+ hour old and have 50+ newer versions in page. By default," .
20            "it will just output how many rows can be deleted. Use the 'prune' option " .
21            "to actually delete them."
22        );
23        $this->addOption( 'prune', 'Actually do a live run' );
24        $this->addOption( 'start', 'The ID of the starting rev', false, true );
25        $this->addOption( 'sleep', 'Extra sleep time between each batch', false, true );
26        $this->setBatchSize( 500 );
27        $this->addOption(
28            'rev-age',
29            'Revisions older than this age will be deleted in seconds (default: 3600)',
30            false,
31            true
32        );
33        $this->addOption(
34            'rev-num',
35            'Revisions must have at least this number of reviewed revisions on top (default: 50)',
36            false,
37            true
38        );
39        $this->requireExtension( 'FlaggedRevs' );
40    }
41
42    /**
43     * @inheritDoc
44     */
45    public function execute() {
46        $start = $this->getOption( 'start' );
47        $prune = $this->getOption( 'prune' );
48        $this->pruneFlaggedRevs( $start, $prune );
49    }
50
51    /**
52     * @param int|null $start Revision ID
53     * @param bool $prune
54     */
55    private function pruneFlaggedRevs( $start = null, $prune = false ) {
56        if ( $prune ) {
57            $this->output( "Pruning old flagged revision inclusion data...\n" );
58        } else {
59            $this->output( "Running dry-run of old flagged revision inclusion data pruning...\n" );
60        }
61
62        $dbw = wfGetDB( DB_PRIMARY );
63        $dbr = wfGetDB( DB_REPLICA );
64
65        if ( $start === null ) {
66            $start = $dbr->selectField( 'flaggedpages', 'MIN(fp_page_id)', false, __METHOD__ );
67        }
68        $end = $dbr->selectField( 'flaggedpages', 'MAX(fp_page_id)', false, __METHOD__ );
69        if ( $start === null || $end === null ) {
70            $this->output( "...flaggedpages table seems to be empty.\n" );
71            return;
72        }
73        $end += $this->mBatchSize - 1; # Do remaining chunk
74        $blockStart = (int)$start;
75        $blockEnd = (int)$start + $this->mBatchSize - 1;
76
77        // Tally
78        $tDeleted = 0;
79
80        $newerRevs = (int)$this->getOption( 'rev-num', 50 );
81        $sleep = (int)$this->getOption( 'sleep', 0 );
82        $cutoff = $dbr->timestamp( time() - (int)$this->getOption( 'rev-age', 3600 ) );
83
84        // First clean up revisions that don't exist anymore.
85        while ( true ) {
86            if ( !$prune ) {
87                break;
88            }
89            $sres = $dbr->selectFieldValues(
90                [ 'flaggedtemplates', 'revision' ],
91                'ft_rev_id',
92                [
93                    'rev_id' => null,
94                ],
95                __METHOD__,
96                [ 'LIMIT' => $this->mBatchSize ],
97                [ 'revision' => [ 'LEFT JOIN', 'rev_id = ft_rev_id' ] ]
98            );
99
100            if ( !$sres ) {
101                break;
102            }
103            $dbw->delete( 'flaggedtemplates',
104                [ 'ft_rev_id' => $sres ],
105                __METHOD__
106            );
107            $rowsCount = $dbw->affectedRows();
108            $this->output( "...deleted $rowsCount rows\n" );
109            $tDeleted += $rowsCount;
110            $this->waitForReplication();
111            sleep( $sleep );
112        }
113
114        while ( $blockEnd <= $end ) {
115            $this->output( "...doing fp_page_id from $blockStart to $blockEnd\n" );
116            $cond = "fp_page_id BETWEEN $blockStart AND $blockEnd";
117            $res = $dbr->select( 'flaggedpages', 'fp_page_id', $cond, __METHOD__ );
118            // Go through a chunk of flagged pages...
119            $revsClearIncludes = [];
120            foreach ( $res as $row ) {
121                // Get the newest X ($newerRevs) flagged revs for this page
122                $sres = $dbr->selectFieldValues( 'flaggedrevs',
123                    'fr_rev_id',
124                    [ 'fr_page_id' => $row->fp_page_id ],
125                    __METHOD__,
126                    [ 'ORDER BY' => 'fr_rev_id DESC', 'LIMIT' => $newerRevs ]
127                );
128                // See if there are older revs that can be pruned...
129                if ( count( $sres ) !== $newerRevs ) {
130                    continue;
131                }
132                // Get the oldest of the top X revisions
133                $oldestId = (int)$sres[ $newerRevs - 1 ];
134                // Get revs not in the top X that were not reviewed recently
135                $sres = $dbr->select( 'flaggedrevs',
136                    'fr_rev_id',
137                    [
138                        'fr_page_id' => $row->fp_page_id,
139                        'fr_rev_id < ' . $oldestId, // not in the newest X
140                        'fr_timestamp < ' . $dbr->addQuotes( $cutoff ) // not reviewed recently
141                    ],
142                    __METHOD__,
143                    [ 'ORDER BY' => 'fr_rev_id ASC', 'LIMIT' => 5000 ]
144                );
145                // Build an array of these rev Ids
146                foreach ( $sres as $srow ) {
147                    $revsClearIncludes[] = $srow->fr_rev_id;
148                }
149            }
150            $blockStart += $this->mBatchSize;
151            $blockEnd += $this->mBatchSize;
152            if ( !$revsClearIncludes ) {
153                continue;
154            } elseif ( $prune ) {
155                foreach ( array_chunk( $revsClearIncludes, $this->mBatchSize ) as $batch ) {
156                    $dbw->delete( 'flaggedtemplates',
157                        [ 'ft_rev_id' => $batch ],
158                        __METHOD__
159                    );
160                    $tDeleted += $dbw->affectedRows();
161                    $this->waitForReplication();
162                    sleep( $sleep );
163                }
164            } else {
165                // Dry run: say how many includes rows would have been cleared
166                $tDeleted += $dbr->selectField( 'flaggedtemplates',
167                    'COUNT(*)',
168                    [ 'ft_rev_id' => $revsClearIncludes ],
169                    __METHOD__
170                );
171            }
172        }
173        if ( $prune ) {
174            $this->output( "Flagged revision inclusion prunning complete ...\n" );
175        } else {
176            $this->output( "Flagged revision inclusion prune test complete ...\n" );
177        }
178        $this->output( "Rows: \tflaggedtemplates:$tDeleted\n" );
179    }
180}
181
182$maintClass = PruneFRIncludeData::class;
183require_once RUN_MAINTENANCE_IF_MAIN;