Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.41% covered (success)
90.41%
66 / 73
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteOldSurveys
98.51% covered (success)
98.51%
66 / 67
50.00% covered (danger)
50.00%
1 / 2
14
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 execute
98.31% covered (success)
98.31%
58 / 59
0.00% covered (danger)
0.00%
0 / 1
13
1<?php
2
3namespace GrowthExperiments\Maintenance;
4
5use GrowthExperiments\WelcomeSurvey;
6use IDBAccessObject;
7use Maintenance;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\User\User;
10use MediaWiki\Utils\MWTimestamp;
11
12$IP = getenv( 'MW_INSTALL_PATH' );
13if ( $IP === false ) {
14    $IP = __DIR__ . '/../../..';
15}
16require_once "$IP/maintenance/Maintenance.php";
17
18/**
19 * Delete welcome surveys older than a cutoff date.
20 */
21class DeleteOldSurveys extends Maintenance {
22
23    public function __construct() {
24        parent::__construct();
25        $this->requireExtension( 'GrowthExperiments' );
26
27        $this->addDescription( 'Delete welcome survey data older than a year.' );
28        $this->addOption( 'cutoff', 'Cutoff interval (data older than this many days will be deleted)',
29            true, true );
30        $this->addOption( 'dry-run', 'Simulate execution without writing any changes' );
31        $this->addOption( 'verbose', 'Verbose output', false, false, 'v' );
32        $this->setBatchSize( 1000 );
33    }
34
35    /** @inheritDoc */
36    public function execute() {
37        $cutoffDays = (int)$this->getOption( 'cutoff' );
38        $dryRun = $this->hasOption( 'dry-run' );
39        $verbose = $this->hasOption( 'verbose' );
40
41        if ( !$cutoffDays ) {
42            $this->fatalError( 'Invalid cutoff period: ' . $this->getOption( 'cutoff' ) );
43        }
44
45        // This seems to be the least ugly way of using a relative date specifier while
46        // keeping MWTimestamp::setFakeTime working.
47        $ts = MWTimestamp::getInstance();
48        $ts->timestamp->modify( "-$cutoffDays day" );
49        $cutoffDate = $ts->getTimestamp( TS_MW );
50        $this->output( "Deleting data before $cutoffDate (over $cutoffDays days old)" .
51            ( $dryRun ? ' (dry run)' : '' ) . "\n" );
52
53        $dbr = $this->getDB( DB_REPLICA );
54        $dbw = $this->getDB( DB_PRIMARY );
55        $fromUserId = 0;
56        $break = false;
57        $userOptionsManager = MediaWikiServices::getInstance()->getUserOptionsManager();
58        do {
59            $res = $dbr->newSelectQueryBuilder()
60                ->select( [ 'user_id', 'up_value', 'user_registration' ] )
61                ->from( 'user_properties' )
62                ->join( 'user', null, [ 'user_id = up_user' ] )
63                ->where( [
64                    'up_property' => WelcomeSurvey::SURVEY_PROP,
65                    $dbr->expr( 'user_id', '>', $fromUserId )
66                ] )
67                ->orderBy( 'user_id ASC' )
68                ->limit( $this->mBatchSize )
69                ->caller( __METHOD__ )->fetchResultSet();
70            $ids = [];
71            $deletedCount = $skippedCount = 0;
72            foreach ( $res as $row ) {
73                $fromUserId = $row->user_id;
74                $userRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
75                $welcomeSurveyData = json_decode( $row->up_value, true );
76                if ( $userRegistration > $cutoffDate ) {
77                    // The submit date cannot be smaller than the registration date, and registration
78                    // date is monotonic; we can stop here.
79                    if ( $verbose ) {
80                        $this->output( "  Stopping at user:$row->user_id which has past-cutoff registration date " .
81                            $userRegistration . "\n" );
82                    }
83                    $break = true;
84                    break;
85                } elseif ( isset( $welcomeSurveyData['_submit_date'] ) &&
86                    $welcomeSurveyData['_submit_date'] > $cutoffDate
87                ) {
88                    // The submit date is not monotonic by user id; we can skip this record but need to
89                    // check later ones.
90                    if ( $verbose ) {
91                        $this->output( "  Skipping user:$row->user_id, past-cutoff survey submit date " .
92                            $welcomeSurveyData['_submit_date'] . "\n" );
93                    }
94                    $skippedCount++;
95                } else {
96                    if ( $verbose ) {
97                        $this->output( "  Deleting survey data for user:$row->user_id\n" );
98                    }
99                    $ids[] = $row->user_id;
100                }
101            }
102            foreach ( $ids as $id ) {
103                if ( !$dryRun ) {
104                    $this->beginTransaction( $dbw, __METHOD__ );
105                    $user = User::newFromId( $id );
106                    $user->load( IDBAccessObject::READ_EXCLUSIVE );
107                    // Setting an option to null will assign it the default value, which in turn
108                    // will delete it (meaning we won't have to reprocess this row on the next run).
109                    $userOptionsManager->setOption( $user, WelcomeSurvey::SURVEY_PROP, null );
110                    $user->saveSettings();
111                    $this->commitTransaction( $dbw, __METHOD__ );
112                }
113                $deletedCount++;
114            }
115            $this->output( "Processed users up to ID $fromUserId\n" );
116        } while ( !$break && $res->numRows() === $this->mBatchSize );
117        $this->output( "Deleted: $deletedCount, skipped: $skippedCount\n" );
118    }
119
120}
121
122$maintClass = DeleteOldSurveys::class;
123require_once RUN_MAINTENANCE_IF_MAIN;