Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.07% covered (danger)
31.07%
32 / 103
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReassignMentees
31.07% covered (danger)
31.07%
32 / 103
0.00% covered (danger)
0.00%
0 / 4
78.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 scheduleReassignMenteesJob
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 reassignMentees
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 doReassignMentees
45.07% covered (danger)
45.07%
32 / 71
0.00% covered (danger)
0.00%
0 / 1
22.42
1<?php
2
3namespace GrowthExperiments\Mentorship;
4
5use GrowthExperiments\Mentorship\Provider\MentorProvider;
6use GrowthExperiments\Mentorship\Store\MentorStore;
7use GrowthExperiments\WikiConfigException;
8use JobSpecification;
9use MediaWiki\JobQueue\JobQueueGroupFactory;
10use MediaWiki\Status\StatusFormatter;
11use MediaWiki\User\UserIdentity;
12use MediaWiki\WikiMap\WikiMap;
13use MessageLocalizer;
14use Psr\Log\LoggerAwareTrait;
15use Psr\Log\NullLogger;
16use Wikimedia\LightweightObjectStore\ExpirationAwareness;
17use Wikimedia\Rdbms\IDatabase;
18
19class ReassignMentees {
20    use LoggerAwareTrait;
21
22    private IDatabase $dbw;
23    private MentorManager $mentorManager;
24    private MentorProvider $mentorProvider;
25    private MentorStore $mentorStore;
26    private ChangeMentorFactory $changeMentorFactory;
27    private JobQueueGroupFactory $jobQueueGroupFactory;
28    private StatusFormatter $statusFormatter;
29    private UserIdentity $performer;
30    private UserIdentity $mentor;
31    private MessageLocalizer $messageLocalizer;
32
33    /**
34     * @param IDatabase $dbw
35     * @param MentorManager $mentorManager
36     * @param MentorProvider $mentorProvider
37     * @param MentorStore $mentorStore
38     * @param ChangeMentorFactory $changeMentorFactory
39     * @param JobQueueGroupFactory $jobQueueGroupFactory
40     * @param StatusFormatter $statusFormatter
41     * @param UserIdentity $performer
42     * @param UserIdentity $mentor
43     * @param MessageLocalizer $messageLocalizer
44     */
45    public function __construct(
46        IDatabase $dbw,
47        MentorManager $mentorManager,
48        MentorProvider $mentorProvider,
49        MentorStore $mentorStore,
50        ChangeMentorFactory $changeMentorFactory,
51        JobQueueGroupFactory $jobQueueGroupFactory,
52        StatusFormatter $statusFormatter,
53        UserIdentity $performer,
54        UserIdentity $mentor,
55        MessageLocalizer $messageLocalizer
56    ) {
57        $this->dbw = $dbw;
58        $this->mentorManager = $mentorManager;
59        $this->mentorProvider = $mentorProvider;
60        $this->mentorStore = $mentorStore;
61        $this->changeMentorFactory = $changeMentorFactory;
62        $this->jobQueueGroupFactory = $jobQueueGroupFactory;
63        $this->statusFormatter = $statusFormatter;
64        $this->performer = $performer;
65        $this->mentor = $mentor;
66        $this->messageLocalizer = $messageLocalizer;
67        $this->logger = new NullLogger();
68    }
69
70    /**
71     * Schedule a new job to reassign mentees
72     *
73     * @internal Only public to be used from ReassignMenteesJob
74     * @param string $reassignMessageKey
75     * @param mixed ...$reassignMessageAdditionalParams
76     */
77    public function scheduleReassignMenteesJob(
78        string $reassignMessageKey,
79       ...$reassignMessageAdditionalParams
80    ) {
81        $jobQueueGroup = $this->jobQueueGroupFactory->makeJobQueueGroup();
82        $jobQueue = $jobQueueGroup->get( ReassignMenteesJob::JOB_NAME );
83
84        $jobParams = [
85            'mentorId' => $this->mentor->getId(),
86            'performerId' => $this->performer->getId(),
87            'reassignMessageKey' => $reassignMessageKey,
88            'reassignMessageAdditionalParams' => $reassignMessageAdditionalParams,
89        ];
90
91        // Opportunistically delay the job by a minute, as a lock handoff
92        // might be happening (T376124).
93        if ( $jobQueue->delayedJobsEnabled() ) {
94            $jobParams['jobReleaseTimestamp'] = (int)wfTimestamp() + ExpirationAwareness::TTL_MINUTE;
95        } else {
96            $this->logger->debug(
97                'ReassignMentees failed to delay reassignMenteesJob, delays are not supported'
98            );
99        }
100
101        $jobQueueGroup->lazyPush(
102            new JobSpecification( ReassignMenteesJob::JOB_NAME, $jobParams )
103        );
104    }
105
106    /**
107     * Reassign mentees currently assigned to the mentor via a job
108     *
109     * If no job is needed, use doReassignMentees directly.
110     *
111     * @param string $reassignMessageKey Message key used in ChangeMentor notification; needs
112     * to accept one parameter (username of the previous mentor). Additional parameters can be
113     * passed via $reassignMessageAdditionalParams.
114     * @param mixed ...$reassignMessageAdditionalParams
115     */
116    public function reassignMentees(
117        string $reassignMessageKey,
118        ...$reassignMessageAdditionalParams
119    ): void {
120        // checking if any mentees exist is a cheap operation; do not submit a job if it is going
121        // to be a no-op.
122        if ( $this->mentorStore->hasAnyMentees( $this->mentor, MentorStore::ROLE_PRIMARY ) ) {
123            $this->scheduleReassignMenteesJob(
124                $reassignMessageKey,
125                ...$reassignMessageAdditionalParams
126            );
127        }
128    }
129
130    /**
131     * Actually reassign all mentees currently assigned to the mentor
132     *
133     * @param int|null $limit Maximum number of mentees processed (null means no limit; if used,
134     * caller is responsible for checking if there are any mentees left)
135     * @param string $reassignMessageKey Message key used in in ChangeMentor notification; needs
136     * to accept one parameter (username of the previous mentor). Additional parameters can be
137     * passed via $reassignMessageAdditionalParams.
138     * @param mixed ...$reassignMessageAdditionalParams
139     * @return bool True if successful, false otherwise.
140     */
141    public function doReassignMentees(
142        ?int $limit,
143        string $reassignMessageKey,
144        ...$reassignMessageAdditionalParams
145    ): bool {
146        $lockName = 'GrowthExperiments-ReassignMentees-' . $this->mentor->getId() .
147            WikiMap::getCurrentWikiId();
148        if ( !$this->dbw->lock( $lockName, __METHOD__, 0 ) ) {
149            $this->logger->warning(
150                __METHOD__ . ' failed to acquire a lock for {mentor}', [
151                    'mentor' => $this->mentor->getName(),
152                ]
153            );
154            return false;
155        }
156
157        // only process primary mentors (T309984). Backup mentors will be automatically ignored by
158        // MentorPageMentorManager::getMentorForUser and replaced with a valid mentor if needed
159        $mentees = $this->mentorStore->getMenteesByMentor( $this->mentor, MentorStore::ROLE_PRIMARY );
160        $this->logger->info( __METHOD__ . ' processing {mentees} mentees', [
161            'mentees' => count( $mentees ),
162        ] );
163        $numberOfProcessedMentees = 0;
164        foreach ( $mentees as $mentee ) {
165            $this->logger->debug( __METHOD__ . ' processing {mentor}', [
166                'mentor' => $mentee->getName(),
167            ] );
168            $changeMentor = $this->changeMentorFactory->newChangeMentor(
169                $mentee,
170                $this->performer
171            );
172
173            try {
174                $newMentor = $this->mentorManager->getRandomAutoAssignedMentor( $mentee );
175            } catch ( WikiConfigException $e ) {
176                $this->logger->warning(
177                    'ReassignMentees failed to reassign mentees for {mentor}; mentor list is invalid',
178                    [
179                        'mentor' => $this->mentor->getName(),
180                    ]
181                );
182                return false;
183            }
184
185            if ( !$newMentor ) {
186                $this->logger->warning(
187                    'ReassignMentees failed to reassign mentees for {mentor}; no mentor is available',
188                    [
189                        'mentor' => $this->mentor->getName(),
190                        'impact' => 'Mentor-mentee relationship dropped',
191                    ]
192                );
193                $this->mentorStore->dropMenteeRelationship( $mentee );
194                continue;
195            }
196
197            $status = $changeMentor->execute(
198                $newMentor,
199                $this->messageLocalizer->msg(
200                    $reassignMessageKey,
201                    $this->mentor->getName(),
202                    ...$reassignMessageAdditionalParams
203                )->text(),
204                true
205            );
206            if ( !$status->isOK() ) {
207                $this->logger->warning(
208                    'ReassignMentees failed to assign {mentor} as {user}\'s mentor for {reason}',
209                    [
210                        'mentor' => $newMentor->getName(),
211                        'user' => $mentee->getName(),
212                        'reason' => $this->statusFormatter->getWikiText( $status, [ 'lang' => 'en' ] ),
213                    ]
214                );
215            }
216
217            $numberOfProcessedMentees += 1;
218            if ( $limit && $numberOfProcessedMentees >= $limit ) {
219                $this->logger->info( 'ReassignMentees processed the maximum number of mentees', [
220                    'limit' => $limit,
221                    'mentor' => $this->mentor->getName(),
222                ] );
223                break;
224            }
225        }
226
227        if ( !$this->dbw->unlock( $lockName, __METHOD__ ) ) {
228            $this->logger->error( 'ReassignMentees failed to release its lock', [
229                'mentor' => $this->mentor->getName(),
230            ] );
231        }
232        return true;
233    }
234}