Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.68% covered (success)
92.68%
76 / 82
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PurgeScoreCache
98.70% covered (success)
98.70%
76 / 77
80.00% covered (warning)
80.00%
4 / 5
15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 purge
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 purgeOld
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
3.00
 deleteRows
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace ORES\Maintenance;
4
5use Maintenance;
6use ORES\Services\ORESServices;
7use Wikimedia\Rdbms\Database;
8
9require_once getenv( 'MW_INSTALL_PATH' ) !== false
10    ? getenv( 'MW_INSTALL_PATH' ) . '/maintenance/Maintenance.php'
11    : __DIR__ . '/../../../maintenance/Maintenance.php';
12
13/**
14 * @ingroup Maintenance
15 */
16class PurgeScoreCache extends Maintenance {
17
18    public function __construct() {
19        parent::__construct();
20
21        $this->requireExtension( 'ORES' );
22        $this->addDescription( 'Purge out of date (or all) ORES model results' );
23
24        $this->addOption( 'model', 'Model name (optional)', false, true );
25        $this->addOption( 'all', 'Flag to indicate that we want to clear all data, ' .
26            'even those from the most recent model', false, false );
27        $this->addOption( 'old', 'Flag to indicate that we only want to clear old data ' .
28            'that is not in recent changes anymore.', false, false );
29        $this->setBatchSize( 1000 );
30    }
31
32    public function execute() {
33        if ( $this->hasOption( 'model' ) ) {
34            $models = [ $this->getOption( 'model' ) ];
35        } else {
36            $models = array_keys( ORESServices::getModelLookup()->getModels() );
37        }
38
39        $batchSize = $this->getBatchSize();
40        $this->output( "Purging ORES scores:\n" );
41        foreach ( $models as $model ) {
42            if ( $this->hasOption( 'old' ) ) {
43                $deletedRows = $this->purgeOld( $model, $batchSize );
44                $description = 'old rows';
45            } elseif ( $this->hasOption( 'all' ) ) {
46                $deletedRows = $this->purge( $model, true, $batchSize );
47                $description = 'scores from all model versions';
48            } else {
49                $deletedRows = $this->purge( $model, false, $batchSize );
50                $description = 'scores from old model versions';
51            }
52            if ( $deletedRows ) {
53                $this->output( "   ...purging $description from '$model' model': deleted $deletedRows rows\n" );
54            } else {
55                $this->output( "   ...skipping '$model' model, no action needed\n" );
56            }
57        }
58        $this->output( "   done.\n" );
59    }
60
61    /**
62     * Delete cached scores
63     *
64     * Normally, we'll only delete scores from out-of-date model versions.
65     *
66     * @param string $model Model name.
67     * @param bool $isEverything When true, delete scores with the up-to-date
68     *   model version as well.  This can be used in cases where the old data is
69     *   bad, but no new model has been released yet.
70     * @param int $batchSize Maximum number of records to delete per loop.
71     *   Note that this function runs multiple batches, until all records are deleted.
72     * @return int The number of deleted rows
73     */
74    private function purge( $model, $isEverything, $batchSize = 1000 ) {
75        $conditions = [
76            'oresm_name' => [ $model, null ],
77        ];
78        if ( !$isEverything ) {
79            $conditions[] = '(oresm_is_current != 1 OR oresm_is_current IS NULL)';
80        }
81
82        $modelIds = $this->getReplicaDB()->selectFieldValues( 'ores_model',
83            'oresm_id',
84            $conditions,
85            __METHOD__
86        );
87        if ( !$modelIds ) {
88            return 0;
89        }
90
91        return $this->deleteRows( [ 'oresc_model' => $modelIds ], $batchSize );
92    }
93
94    /**
95     * Delete old cached scores.
96     * A score is old of the corresponding revision is not in the recentchanges table.
97     * @param string $model Model name.
98     * @param int $batchSize Maximum number of records to delete per loop.
99     *   Note that this function runs multiple batches, until all records are deleted.
100     * @return int The number of deleted rows
101     */
102    public function purgeOld( $model, $batchSize = 1000 ) {
103        $dbr = $this->getReplicaDB();
104        $modelIds = $dbr->selectFieldValues( 'ores_model',
105            'oresm_id',
106            [ 'oresm_name' => [ $model, null ] ],
107            __METHOD__
108        );
109
110        $lowestRCRev = $dbr->selectFieldValues( 'recentchanges',
111            'rc_this_oldid',
112            [],
113            __METHOD__,
114            [ 'LIMIT' => 1, 'ORDER BY' => 'rc_id' ]
115        );
116
117        if ( !$lowestRCRev || !$modelIds ) {
118            return 0;
119        }
120
121        $conditions = [
122            $dbr->expr( 'oresc_rev', '<', $lowestRCRev[0] ),
123            'oresc_model' => $modelIds
124        ];
125        return $this->deleteRows( $conditions, $batchSize );
126    }
127
128    /**
129     * Delete cached scores. Which rows to delete is given by Database::select parameters.
130     * @param array $conditions
131     * @param int $batchSize Maximum number of records to delete per loop.
132     *   Note that this function runs multiple batches, until all records are deleted.
133     * @return int The number of deleted rows
134     * @see Database::select
135     */
136    private function deleteRows( array $conditions, $batchSize ) {
137        $dbr = $this->getReplicaDB();
138        $dbw = $this->getPrimaryDB();
139
140        $deletedRows = 0;
141
142        do {
143            $ids = $dbr->selectFieldValues( 'ores_classification',
144                'oresc_id',
145                $conditions,
146                __METHOD__,
147                [ 'LIMIT' => $batchSize ]
148            );
149            if ( $ids ) {
150                $dbw->delete( 'ores_classification',
151                    [ 'oresc_id' => $ids ],
152                    __METHOD__
153                );
154                $deletedRows += $dbw->affectedRows();
155                $this->waitForReplication();
156            }
157        } while ( $ids );
158
159        return $deletedRows;
160    }
161
162}
163
164$maintClass = PurgeScoreCache::class;
165require_once RUN_MAINTENANCE_IF_MAIN;