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