Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.00% |
63 / 70 |
|
75.00% |
9 / 12 |
CRAP | |
0.00% |
0 / 1 |
DeferredUpdatesScope | |
91.30% |
63 / 69 |
|
75.00% |
9 / 12 |
35.81 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
newRootScope | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newChildScope | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getActiveUpdate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addUpdate | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
pendingUpdatesCount | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPendingUpdates | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
clearPendingUpdates | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
consumeMatchingUpdates | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
7 | |||
processUpdates | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
upmergeUnreadyUpdates | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
6.97 | |||
processStageQueue | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
6.05 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Deferred; |
22 | |
23 | /** |
24 | * DeferredUpdates helper class for managing DeferrableUpdate::doUpdate() nesting levels |
25 | * caused by nested calls to DeferredUpdates::doUpdates() |
26 | * |
27 | * @see DeferredUpdates |
28 | * @see DeferredUpdatesScopeStack |
29 | * @internal For use by DeferredUpdates and DeferredUpdatesScopeStack only |
30 | * @since 1.36 |
31 | */ |
32 | class DeferredUpdatesScope { |
33 | /** @var DeferredUpdatesScope|null Parent scope (root scope as none) */ |
34 | private $parentScope; |
35 | /** @var DeferrableUpdate|null Deferred update that owns this scope (root scope has none) */ |
36 | private $activeUpdate; |
37 | /** @var int|null Active processing stage in DeferredUpdates::STAGES (if any) */ |
38 | private $activeStage; |
39 | /** @var DeferrableUpdate[][] Stage-ordered (stage => merge class or position => update) map */ |
40 | private $queueByStage; |
41 | |
42 | /** |
43 | * @param int|null $activeStage One of DeferredUpdates::STAGES or null |
44 | * @param DeferrableUpdate|null $update |
45 | * @param DeferredUpdatesScope|null $parentScope |
46 | */ |
47 | private function __construct( |
48 | $activeStage, |
49 | ?DeferrableUpdate $update, |
50 | ?DeferredUpdatesScope $parentScope |
51 | ) { |
52 | $this->activeStage = $activeStage; |
53 | $this->activeUpdate = $update; |
54 | $this->parentScope = $parentScope; |
55 | $this->queueByStage = array_fill_keys( DeferredUpdates::STAGES, [] ); |
56 | } |
57 | |
58 | /** |
59 | * @return DeferredUpdatesScope Scope for the case of no in-progress deferred update |
60 | */ |
61 | public static function newRootScope() { |
62 | return new self( null, null, null ); |
63 | } |
64 | |
65 | /** |
66 | * @param int $activeStage The in-progress stage; one of DeferredUpdates::STAGES |
67 | * @param DeferrableUpdate $update The deferred update that owns this scope |
68 | * @param DeferredUpdatesScope $parentScope The parent scope of this scope |
69 | * @return DeferredUpdatesScope Scope for the case of an in-progress deferred update |
70 | */ |
71 | public static function newChildScope( |
72 | $activeStage, |
73 | DeferrableUpdate $update, |
74 | DeferredUpdatesScope $parentScope |
75 | ) { |
76 | return new self( $activeStage, $update, $parentScope ); |
77 | } |
78 | |
79 | /** |
80 | * Get the deferred update that owns this scope (root scope has none) |
81 | * |
82 | * @return DeferrableUpdate|null |
83 | */ |
84 | public function getActiveUpdate() { |
85 | return $this->activeUpdate; |
86 | } |
87 | |
88 | /** |
89 | * Enqueue a deferred update within this scope using the specified "defer until" time |
90 | * |
91 | * @param DeferrableUpdate $update |
92 | * @param int $stage One of DeferredUpdates::STAGES |
93 | */ |
94 | public function addUpdate( DeferrableUpdate $update, $stage ) { |
95 | // Handle the case where the specified stage must have already passed |
96 | $stageEffective = max( $stage, $this->activeStage ); |
97 | |
98 | $queue =& $this->queueByStage[$stageEffective]; |
99 | |
100 | if ( $update instanceof MergeableUpdate ) { |
101 | $class = get_class( $update ); // fully-qualified class |
102 | if ( isset( $queue[$class] ) ) { |
103 | /** @var MergeableUpdate $existingUpdate */ |
104 | $existingUpdate = $queue[$class]; |
105 | '@phan-var MergeableUpdate $existingUpdate'; |
106 | $existingUpdate->merge( $update ); |
107 | // Move the update to the end to handle things like mergeable purge |
108 | // updates that might depend on the prior updates in the queue running |
109 | unset( $queue[$class] ); |
110 | $queue[$class] = $existingUpdate; |
111 | } else { |
112 | $queue[$class] = $update; |
113 | } |
114 | } else { |
115 | $queue[] = $update; |
116 | } |
117 | } |
118 | |
119 | /** |
120 | * Get the number of pending updates within this scope |
121 | * |
122 | * @return int |
123 | */ |
124 | public function pendingUpdatesCount() { |
125 | return array_sum( array_map( 'count', $this->queueByStage ) ); |
126 | } |
127 | |
128 | /** |
129 | * Get pending updates within this scope with the given "defer until" stage |
130 | * |
131 | * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL |
132 | * @return DeferrableUpdate[] |
133 | */ |
134 | public function getPendingUpdates( $stage ) { |
135 | $matchingQueues = []; |
136 | foreach ( $this->queueByStage as $queueStage => $queue ) { |
137 | if ( $stage === DeferredUpdates::ALL || $stage === $queueStage ) { |
138 | $matchingQueues[] = $queue; |
139 | } |
140 | } |
141 | |
142 | return array_merge( ...$matchingQueues ); |
143 | } |
144 | |
145 | /** |
146 | * Cancel all pending updates within this scope |
147 | */ |
148 | public function clearPendingUpdates() { |
149 | $this->queueByStage = array_fill_keys( array_keys( $this->queueByStage ), [] ); |
150 | } |
151 | |
152 | /** |
153 | * Remove pending updates of the specified stage/class and pass them to a callback |
154 | * |
155 | * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL |
156 | * @param string $class Only take updates of this fully qualified class/interface name |
157 | * @param callable $callback Callback that takes DeferrableUpdate |
158 | */ |
159 | public function consumeMatchingUpdates( $stage, $class, callable $callback ) { |
160 | // T268840: defensively claim the pending updates in case of recursion |
161 | $claimedUpdates = []; |
162 | foreach ( $this->queueByStage as $queueStage => $queue ) { |
163 | if ( $stage === DeferredUpdates::ALL || $stage === $queueStage ) { |
164 | foreach ( $queue as $k => $update ) { |
165 | if ( $update instanceof $class ) { |
166 | $claimedUpdates[] = $update; |
167 | unset( $this->queueByStage[$queueStage][$k] ); |
168 | } |
169 | } |
170 | } |
171 | } |
172 | // Execute the callback for each update |
173 | foreach ( $claimedUpdates as $update ) { |
174 | $callback( $update ); |
175 | } |
176 | } |
177 | |
178 | /** |
179 | * Iteratively, reassign unready pending updates to the parent scope (if applicable) and |
180 | * process the ready pending updates in stage-order with the callback, repeating the process |
181 | * until there is nothing left to do |
182 | * |
183 | * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL |
184 | * @param callable $callback Processing function with arguments (update, effective stage) |
185 | */ |
186 | public function processUpdates( $stage, callable $callback ) { |
187 | if ( $stage === DeferredUpdates::ALL ) { |
188 | // Do everything, all the way to the last "defer until" stage |
189 | $activeStage = DeferredUpdates::STAGES[count( DeferredUpdates::STAGES ) - 1]; |
190 | } else { |
191 | // Handle the case where the specified stage must have already passed |
192 | $activeStage = max( $stage, $this->activeStage ); |
193 | } |
194 | |
195 | do { |
196 | $processed = $this->upmergeUnreadyUpdates( $activeStage ); |
197 | foreach ( range( DeferredUpdates::STAGES[0], $activeStage ) as $queueStage ) { |
198 | $processed += $this->processStageQueue( $queueStage, $activeStage, $callback ); |
199 | } |
200 | } while ( $processed > 0 ); |
201 | } |
202 | |
203 | /** |
204 | * If this is a child scope, then reassign unready pending updates to the parent scope: |
205 | * - MergeableUpdate instances will be reassigned to the parent scope on account of their |
206 | * de-duplication/melding semantics. They are normally only processed in the root scope. |
207 | * - DeferrableUpdate instances with a "defer until" stage later than the specified stage |
208 | * will be reassigned to the parent scope since they are not ready. |
209 | * |
210 | * @param int $activeStage One of DeferredUpdates::STAGES |
211 | * @return int Number of updates moved |
212 | */ |
213 | private function upmergeUnreadyUpdates( $activeStage ) { |
214 | $reassigned = 0; |
215 | |
216 | if ( !$this->parentScope ) { |
217 | return $reassigned; |
218 | } |
219 | |
220 | foreach ( $this->queueByStage as $queueStage => $queue ) { |
221 | foreach ( $queue as $k => $update ) { |
222 | if ( $update instanceof MergeableUpdate || $queueStage > $activeStage ) { |
223 | unset( $this->queueByStage[$queueStage][$k] ); |
224 | $this->parentScope->addUpdate( $update, $queueStage ); |
225 | ++$reassigned; |
226 | } |
227 | } |
228 | } |
229 | |
230 | return $reassigned; |
231 | } |
232 | |
233 | /** |
234 | * @param int $stage One of DeferredUpdates::STAGES |
235 | * @param int $activeStage One of DeferredUpdates::STAGES |
236 | * @param callable $callback Processing function with arguments (update, effective stage) |
237 | * @return int Number of updates processed |
238 | */ |
239 | private function processStageQueue( $stage, $activeStage, callable $callback ) { |
240 | $processed = 0; |
241 | |
242 | // Defensively claim the pending updates in case of recursion |
243 | $claimedUpdates = $this->queueByStage[$stage]; |
244 | $this->queueByStage[$stage] = []; |
245 | |
246 | // Keep doing rounds of updates until none get enqueued... |
247 | while ( $claimedUpdates ) { |
248 | // Segregate the updates into one for DataUpdate and one for everything else. |
249 | // This is done for historical reasons; DataUpdate used to have its own static |
250 | // method for running DataUpdate instances and was called first in DeferredUpdates. |
251 | // Before that, page updater code directly ran that static method. |
252 | // @TODO: remove this logic given the existence of RefreshSecondaryDataUpdate |
253 | $claimedDataUpdates = []; |
254 | $claimedGenericUpdates = []; |
255 | foreach ( $claimedUpdates as $claimedUpdate ) { |
256 | if ( $claimedUpdate instanceof DataUpdate ) { |
257 | $claimedDataUpdates[] = $claimedUpdate; |
258 | } else { |
259 | $claimedGenericUpdates[] = $claimedUpdate; |
260 | } |
261 | ++$processed; |
262 | } |
263 | |
264 | // Execute the DataUpdate queue followed by the DeferrableUpdate queue... |
265 | foreach ( $claimedDataUpdates as $claimedDataUpdate ) { |
266 | $callback( $claimedDataUpdate, $activeStage ); |
267 | } |
268 | foreach ( $claimedGenericUpdates as $claimedGenericUpdate ) { |
269 | $callback( $claimedGenericUpdate, $activeStage ); |
270 | } |
271 | |
272 | // Check for new entries; defensively claim the pending updates in case of recursion |
273 | $claimedUpdates = $this->queueByStage[$stage]; |
274 | $this->queueByStage[$stage] = []; |
275 | } |
276 | |
277 | return $processed; |
278 | } |
279 | } |
280 | |
281 | /** @deprecated class alias since 1.42 */ |
282 | class_alias( DeferredUpdatesScope::class, 'DeferredUpdatesScope' ); |