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