Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.00% covered (success)
90.00%
63 / 70
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeferredUpdatesScope
91.30% covered (success)
91.30%
63 / 69
75.00% covered (warning)
75.00%
9 / 12
35.81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 newRootScope
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newChildScope
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActiveUpdate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addUpdate
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 pendingUpdatesCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPendingUpdates
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 clearPendingUpdates
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 consumeMatchingUpdates
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 processUpdates
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 upmergeUnreadyUpdates
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
6.97
 processStageQueue
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
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
21namespace 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 */
32class 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 */
282class_alias( DeferredUpdatesScope::class, 'DeferredUpdatesScope' );