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