Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.52% covered (warning)
88.52%
54 / 61
72.73% covered (warning)
72.73%
8 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeferredUpdatesScope
90.00% covered (success)
90.00%
54 / 60
72.73% covered (warning)
72.73%
8 / 11
28.78
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
 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 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Deferred;
8
9/**
10 * DeferredUpdates helper class for managing DeferrableUpdate::doUpdate() nesting levels
11 * caused by nested calls to DeferredUpdates::doUpdates()
12 *
13 * @see DeferredUpdates
14 * @see DeferredUpdatesScopeStack
15 * @internal For use by DeferredUpdates and DeferredUpdatesScopeStack only
16 * @since 1.36
17 */
18class DeferredUpdatesScope {
19    /** @var DeferredUpdatesScope|null Parent scope (root scope as none) */
20    private $parentScope;
21    /** @var DeferrableUpdate|null Deferred update that owns this scope (root scope has none) */
22    private $activeUpdate;
23    /** @var int|null Active processing stage in DeferredUpdates::STAGES (if any) */
24    private $activeStage;
25    /** @var DeferrableUpdate[][] Stage-ordered (stage => merge class or position => update) map */
26    private $queueByStage;
27
28    /**
29     * @param int|null $activeStage One of DeferredUpdates::STAGES or null
30     * @param DeferrableUpdate|null $update
31     * @param DeferredUpdatesScope|null $parentScope
32     */
33    private function __construct(
34        $activeStage,
35        ?DeferrableUpdate $update,
36        ?DeferredUpdatesScope $parentScope
37    ) {
38        $this->activeStage = $activeStage;
39        $this->activeUpdate = $update;
40        $this->parentScope = $parentScope;
41        $this->queueByStage = array_fill_keys( DeferredUpdates::STAGES, [] );
42    }
43
44    /**
45     * @return DeferredUpdatesScope Scope for the case of no in-progress deferred update
46     */
47    public static function newRootScope() {
48        return new self( null, null, null );
49    }
50
51    /**
52     * @param int $activeStage The in-progress stage; one of DeferredUpdates::STAGES
53     * @param DeferrableUpdate $update The deferred update that owns this scope
54     * @param DeferredUpdatesScope $parentScope The parent scope of this scope
55     * @return DeferredUpdatesScope Scope for the case of an in-progress deferred update
56     */
57    public static function newChildScope(
58        $activeStage,
59        DeferrableUpdate $update,
60        DeferredUpdatesScope $parentScope
61    ) {
62        return new self( $activeStage, $update, $parentScope );
63    }
64
65    /**
66     * Get the deferred update that owns this scope (root scope has none)
67     *
68     * @return DeferrableUpdate|null
69     */
70    public function getActiveUpdate() {
71        return $this->activeUpdate;
72    }
73
74    /**
75     * Enqueue a deferred update within this scope using the specified "defer until" time
76     *
77     * @param DeferrableUpdate $update
78     * @param int $stage One of DeferredUpdates::STAGES
79     */
80    public function addUpdate( DeferrableUpdate $update, $stage ) {
81        // Handle the case where the specified stage must have already passed
82        $stageEffective = max( $stage, $this->activeStage );
83
84        $queue =& $this->queueByStage[$stageEffective];
85
86        if ( $update instanceof MergeableUpdate ) {
87            $class = get_class( $update ); // fully-qualified class
88            if ( isset( $queue[$class] ) ) {
89                /** @var MergeableUpdate $existingUpdate */
90                $existingUpdate = $queue[$class];
91                '@phan-var MergeableUpdate $existingUpdate';
92                $existingUpdate->merge( $update );
93                // Move the update to the end to handle things like mergeable purge
94                // updates that might depend on the prior updates in the queue running
95                unset( $queue[$class] );
96                $queue[$class] = $existingUpdate;
97            } else {
98                $queue[$class] = $update;
99            }
100        } else {
101            $queue[] = $update;
102        }
103    }
104
105    /**
106     * Get the number of pending updates within this scope
107     *
108     * @return int
109     */
110    public function pendingUpdatesCount() {
111        return array_sum( array_map( 'count', $this->queueByStage ) );
112    }
113
114    /**
115     * Get pending updates within this scope with the given "defer until" stage
116     *
117     * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL
118     * @return DeferrableUpdate[]
119     */
120    public function getPendingUpdates( $stage ) {
121        $matchingQueues = [];
122        foreach ( $this->queueByStage as $queueStage => $queue ) {
123            if ( $stage === DeferredUpdates::ALL || $stage === $queueStage ) {
124                $matchingQueues[] = $queue;
125            }
126        }
127
128        return array_merge( ...$matchingQueues );
129    }
130
131    /**
132     * Cancel all pending updates within this scope
133     */
134    public function clearPendingUpdates() {
135        $this->queueByStage = array_fill_keys( array_keys( $this->queueByStage ), [] );
136    }
137
138    /**
139     * Iteratively, reassign unready pending updates to the parent scope (if applicable) and
140     * process the ready pending updates in stage-order with the callback, repeating the process
141     * until there is nothing left to do
142     *
143     * @param int $stage One of DeferredUpdates::STAGES or DeferredUpdates::ALL
144     * @param callable $callback Processing function with arguments (update, effective stage)
145     */
146    public function processUpdates( $stage, callable $callback ) {
147        if ( $stage === DeferredUpdates::ALL ) {
148            // Do everything, all the way to the last "defer until" stage
149            $activeStage = DeferredUpdates::STAGES[count( DeferredUpdates::STAGES ) - 1];
150        } else {
151            // Handle the case where the specified stage must have already passed
152            $activeStage = max( $stage, $this->activeStage );
153        }
154
155        do {
156            $processed = $this->upmergeUnreadyUpdates( $activeStage );
157            foreach ( range( DeferredUpdates::STAGES[0], $activeStage ) as $queueStage ) {
158                $processed += $this->processStageQueue( $queueStage, $activeStage, $callback );
159            }
160        } while ( $processed > 0 );
161    }
162
163    /**
164     * If this is a child scope, then reassign unready pending updates to the parent scope:
165     *   - MergeableUpdate instances will be reassigned to the parent scope on account of their
166     *     de-duplication/melding semantics. They are normally only processed in the root scope.
167     *   - DeferrableUpdate instances with a "defer until" stage later than the specified stage
168     *     will be reassigned to the parent scope since they are not ready.
169     *
170     * @param int $activeStage One of DeferredUpdates::STAGES
171     * @return int Number of updates moved
172     */
173    private function upmergeUnreadyUpdates( $activeStage ) {
174        $reassigned = 0;
175
176        if ( !$this->parentScope ) {
177            return $reassigned;
178        }
179
180        foreach ( $this->queueByStage as $queueStage => $queue ) {
181            foreach ( $queue as $k => $update ) {
182                if ( $update instanceof MergeableUpdate || $queueStage > $activeStage ) {
183                    unset( $this->queueByStage[$queueStage][$k] );
184                    $this->parentScope->addUpdate( $update, $queueStage );
185                    ++$reassigned;
186                }
187            }
188        }
189
190        return $reassigned;
191    }
192
193    /**
194     * @param int $stage One of DeferredUpdates::STAGES
195     * @param int $activeStage One of DeferredUpdates::STAGES
196     * @param callable $callback Processing function with arguments (update, effective stage)
197     * @return int Number of updates processed
198     */
199    private function processStageQueue( $stage, $activeStage, callable $callback ) {
200        $processed = 0;
201
202        // Defensively claim the pending updates in case of recursion
203        $claimedUpdates = $this->queueByStage[$stage];
204        $this->queueByStage[$stage] = [];
205
206        // Keep doing rounds of updates until none get enqueued...
207        while ( $claimedUpdates ) {
208            // Segregate the updates into one for DataUpdate and one for everything else.
209            // This is done for historical reasons; DataUpdate used to have its own static
210            // method for running DataUpdate instances and was called first in DeferredUpdates.
211            // Before that, page updater code directly ran that static method.
212            // @TODO: remove this logic given the existence of RefreshSecondaryDataUpdate
213            $claimedDataUpdates = [];
214            $claimedGenericUpdates = [];
215            foreach ( $claimedUpdates as $claimedUpdate ) {
216                if ( $claimedUpdate instanceof DataUpdate ) {
217                    $claimedDataUpdates[] = $claimedUpdate;
218                } else {
219                    $claimedGenericUpdates[] = $claimedUpdate;
220                }
221                ++$processed;
222            }
223
224            // Execute the DataUpdate queue followed by the DeferrableUpdate queue...
225            foreach ( $claimedDataUpdates as $claimedDataUpdate ) {
226                $callback( $claimedDataUpdate, $activeStage );
227            }
228            foreach ( $claimedGenericUpdates as $claimedGenericUpdate ) {
229                $callback( $claimedGenericUpdate, $activeStage );
230            }
231
232            // Check for new entries;  defensively claim the pending updates in case of recursion
233            $claimedUpdates = $this->queueByStage[$stage];
234            $this->queueByStage[$stage] = [];
235        }
236
237        return $processed;
238    }
239}
240
241/** @deprecated class alias since 1.42 */
242class_alias( DeferredUpdatesScope::class, 'DeferredUpdatesScope' );