Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateMenteeData
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 5
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 initServices
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 addProfilingInfoForMentor
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getSummarizedProfilingInfoInSeconds
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
210
1<?php
2
3namespace GrowthExperiments\Maintenance;
4
5use GrowthExperiments\GrowthExperimentsServices;
6use GrowthExperiments\MentorDashboard\MenteeOverview\MenteeOverviewDataUpdater;
7use GrowthExperiments\Mentorship\Provider\MentorProvider;
8use GrowthExperiments\WikiConfigException;
9use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
10use Maintenance;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\User\UserFactory;
13use Wikimedia\Rdbms\ILoadBalancer;
14
15$IP = getenv( 'MW_INSTALL_PATH' );
16if ( $IP === false ) {
17    $IP = __DIR__ . '/../../..';
18}
19require_once "$IP/maintenance/Maintenance.php";
20
21class UpdateMenteeData extends Maintenance {
22
23    /** @var MenteeOverviewDataUpdater */
24    private $menteeOverviewDataUpdater;
25
26    /** @var MentorProvider */
27    private $mentorProvider;
28
29    /** @var UserFactory */
30    private $userFactory;
31
32    /** @var ILoadBalancer */
33    private $growthLoadBalancer;
34
35    /** @var StatsdDataFactoryInterface */
36    private $dataFactory;
37
38    /** @var array */
39    private $detailedProfilingInfo = [];
40
41    public function __construct() {
42        parent::__construct();
43        $this->setBatchSize( 200 );
44        $this->requireExtension( 'GrowthExperiments' );
45
46        $this->addDescription( 'Update growthexperiments_mentee_data database table' );
47        $this->addOption( 'force', 'Do the update even if GEMentorDashboardEnabled is false' );
48        $this->addOption( 'mentor', 'Username of the mentor to update the data for', false, true );
49        $this->addOption( 'statsd', 'Send timing information to statsd' );
50        $this->addOption( 'verbose', 'Output detailed profiling information' );
51        $this->addOption(
52            'dbshard',
53            'ID of the DB shard this script runs at',
54            false,
55            true
56        );
57    }
58
59    private function initServices() {
60        $services = MediaWikiServices::getInstance();
61        $geServices = GrowthExperimentsServices::wrap( $services );
62
63        $this->menteeOverviewDataUpdater = $geServices->getMenteeOverviewDataUpdater();
64        $this->menteeOverviewDataUpdater->setBatchSize( $this->getBatchSize() );
65        $this->mentorProvider = $geServices->getMentorProvider();
66        $this->userFactory = $services->getUserFactory();
67        $this->growthLoadBalancer = $geServices->getLoadBalancer();
68        $this->dataFactory = $services->getStatsdDataFactory();
69    }
70
71    private function addProfilingInfoForMentor( array $mentorProfilingInfo ): void {
72        foreach ( $mentorProfilingInfo as $section => $seconds ) {
73            if ( !array_key_exists( $section, $this->detailedProfilingInfo ) ) {
74                $this->detailedProfilingInfo[$section] = 0;
75            }
76
77            $this->detailedProfilingInfo[$section] += $seconds;
78        }
79    }
80
81    private function getSummarizedProfilingInfoInSeconds(): array {
82        $res = [];
83        foreach ( $this->detailedProfilingInfo as $section => $seconds ) {
84            $res[$section] = round( $seconds, 2 );
85        }
86        return $res;
87    }
88
89    public function execute() {
90        if (
91            !$this->getConfig()->get( 'GEMentorDashboardEnabled' ) &&
92            !$this->hasOption( 'force' )
93        ) {
94            $this->output( "Mentor dashboard is disabled.\n" );
95            return;
96        }
97
98        $this->initServices();
99
100        $startTime = time();
101
102        if ( $this->hasOption( 'mentor' ) ) {
103            $mentors = [ $this->getOption( 'mentor' ) ];
104        } else {
105            try {
106                $mentors = $this->mentorProvider->getMentors();
107            } catch ( WikiConfigException $e ) {
108                $this->fatalError( 'List of mentors cannot be fetched.' );
109            }
110        }
111
112        $allUpdatedMenteeIds = [];
113        $dbw = $this->growthLoadBalancer->getConnection( DB_PRIMARY );
114        foreach ( $mentors as $mentorRaw ) {
115            $mentor = $this->userFactory->newFromName( $mentorRaw );
116            if ( $mentor === null ) {
117                $this->output( 'Skipping ' . $mentorRaw . ", invalid user\n" );
118                continue;
119            }
120
121            $updatedMenteeIds = $this->menteeOverviewDataUpdater->updateDataForMentor( $mentor );
122            $allUpdatedMenteeIds = array_merge( $allUpdatedMenteeIds, $updatedMenteeIds );
123            $this->addProfilingInfoForMentor(
124                $this->menteeOverviewDataUpdater->getMentorProfilingInfo()
125            );
126        }
127
128        // Delete all mentees recorded in the table which were not updated
129        // This cannot happen when --mentor was passed, as that would delete
130        // most of the data.
131        if ( !$this->hasOption( 'mentor' ) ) {
132            $menteeIdsToDelete = array_diff(
133                array_map(
134                    'intval',
135                    $dbw->newSelectQueryBuilder()
136                        ->select( 'mentee_id' )
137                        ->from( 'growthexperiments_mentee_data' )
138                        ->caller( __METHOD__ )->fetchFieldValues()
139                ),
140                $allUpdatedMenteeIds
141            );
142            if ( $menteeIdsToDelete !== [] ) {
143                $dbw->newDeleteQueryBuilder()
144                    ->deleteFrom( 'growthexperiments_mentee_data' )
145                    ->where( [
146                        'mentee_id' => $menteeIdsToDelete
147                    ] )
148                    ->caller( __METHOD__ )
149                    ->execute();
150            }
151        }
152
153        $totalTime = time() - $startTime;
154        $profilingInfo = $this->getSummarizedProfilingInfoInSeconds();
155
156        if ( $this->hasOption( 'verbose' ) ) {
157            $this->output( "Profiling data:\n" );
158            foreach ( $profilingInfo as $section => $seconds ) {
159                $this->output( "  * {$section}{$seconds} seconds\n" );
160            }
161            $this->output( "===============\n" );
162        }
163
164        if ( $this->hasOption( 'statsd' ) && $this->hasOption( 'dbshard' ) ) {
165            $this->dataFactory->timing(
166                'timing.growthExperiments.updateMenteeData.' . $this->getOption( 'dbshard' ) . '.total',
167                $totalTime
168            );
169
170            foreach ( $profilingInfo as $section => $seconds ) {
171                $this->dataFactory->timing(
172                    'timing.growthExperiments.updateMenteeData.' .
173                    $this->getOption( 'dbshard' ) .
174                    '.' .
175                    $section,
176                    $seconds
177                );
178            }
179        }
180
181        $this->output( "Done. Took {$totalTime} seconds.\n" );
182    }
183}
184
185$maintClass = UpdateMenteeData::class;
186require_once RUN_MAINTENANCE_IF_MAIN;