Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
31.07% |
32 / 103 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
ReassignMentees | |
31.07% |
32 / 103 |
|
0.00% |
0 / 4 |
78.20 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
scheduleReassignMenteesJob | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
reassignMentees | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
doReassignMentees | |
45.07% |
32 / 71 |
|
0.00% |
0 / 1 |
22.42 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Mentorship; |
4 | |
5 | use GrowthExperiments\Mentorship\Provider\MentorProvider; |
6 | use GrowthExperiments\Mentorship\Store\MentorStore; |
7 | use GrowthExperiments\WikiConfigException; |
8 | use JobSpecification; |
9 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
10 | use MediaWiki\Status\StatusFormatter; |
11 | use MediaWiki\User\UserIdentity; |
12 | use MediaWiki\WikiMap\WikiMap; |
13 | use MessageLocalizer; |
14 | use Psr\Log\LoggerAwareTrait; |
15 | use Psr\Log\NullLogger; |
16 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
17 | use Wikimedia\Rdbms\IDatabase; |
18 | |
19 | class 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 | } |