MediaWiki master
DeferredUpdates.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Deferred;
8
9use LogicException;
13use Throwable;
15use Wikimedia\ScopedCallback;
16
87 public const ALL = 0;
89 public const PRESEND = 1;
91 public const POSTSEND = 2;
92
94 public const STAGES = [ self::PRESEND, self::POSTSEND ];
95
97 private static $scopeStack;
98
102 private static $preventOpportunisticUpdates = 0;
103
104 private static function getScopeStack(): DeferredUpdatesScopeStack {
105 self::$scopeStack ??= new DeferredUpdatesScopeMediaWikiStack();
106 return self::$scopeStack;
107 }
108
113 public static function setScopeStack( DeferredUpdatesScopeStack $scopeStack ): void {
114 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
115 throw new LogicException( 'Cannot reconfigure DeferredUpdates outside tests' );
116 }
117 self::$scopeStack = $scopeStack;
118 }
119
140 public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
141 self::getScopeStack()->current()->addUpdate( $update, $stage );
142 self::tryOpportunisticExecute();
143 }
144
160 public static function addCallableUpdate(
161 $callable,
162 $stage = self::POSTSEND,
163 $dependeeDbws = []
164 ) {
165 self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dependeeDbws ), $stage );
166 }
167
175 private static function run( DeferrableUpdate $update ): ?Throwable {
176 $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
177
178 $type = get_class( $update )
179 . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' );
180 $updateId = spl_object_id( $update );
181 $logger->debug( "DeferredUpdates::run: started $type #{updateId}", [ 'updateId' => $updateId ] );
182
183 $updateException = null;
184
185 $startTime = microtime( true );
186 try {
187 self::attemptUpdate( $update );
188 } catch ( Throwable $updateException ) {
189 MWExceptionHandler::logException( $updateException );
190 $logger->error(
191 "Deferred update '{deferred_type}' failed to run.",
192 [
193 'deferred_type' => $type,
194 'exception' => $updateException,
195 ]
196 );
197 self::getScopeStack()->onRunUpdateFailed( $update );
198 } finally {
199 $walltime = microtime( true ) - $startTime;
200 $logger->debug( "DeferredUpdates::run: ended $type #{updateId}, processing time: {walltime}", [
201 'updateId' => $updateId,
202 'walltime' => $walltime,
203 ] );
204 }
205
206 // Try to push the update as a job so it can run later if possible
207 if ( $updateException && $update instanceof EnqueueableDataUpdate ) {
208 try {
209 self::getScopeStack()->queueDataUpdate( $update );
210 } catch ( Throwable $jobException ) {
211 MWExceptionHandler::logException( $jobException );
212 $logger->error(
213 "Deferred update '{deferred_type}' failed to enqueue as a job.",
214 [
215 'deferred_type' => $type,
216 'exception' => $jobException,
217 ]
218 );
219 self::getScopeStack()->onRunUpdateFailed( $update );
220 }
221 }
222
223 return $updateException;
224 }
225
244 public static function doUpdates( $stage = self::ALL ) {
246 $guiError = null;
248 $exception = null;
249
250 $scope = self::getScopeStack()->current();
251
252 // T249069: recursion is not possible once explicit transaction rounds are involved
253 $activeUpdate = $scope->getActiveUpdate();
254 if ( $activeUpdate ) {
255 $class = get_class( $activeUpdate );
256 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
257 throw new LogicException(
258 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
259 );
260 }
261 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
262 throw new LogicException(
263 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
264 );
265 }
266 }
267
268 $scope->processUpdates(
269 $stage,
270 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
271 $scopeStack = self::getScopeStack();
272 $childScope = $scopeStack->descend( $activeStage, $update );
273 try {
274 $e = self::run( $update );
275 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
276 $exception = $exception ?: $e;
277 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
278 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
279 // doUpdates() itself. In any case, process remaining updates in the subqueue.
280 // them, enqueueing them, or transferring them to the parent scope
281 // queues as appropriate...
282 $childScope->processUpdates(
283 $activeStage,
284 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
285 $e = self::run( $sub );
286 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
287 $exception = $exception ?: $e;
288 }
289 );
290 } finally {
291 $scopeStack->ascend();
292 }
293 }
294 );
295
296 // VW-style hack to work around T190178, so we can make sure
297 // PageMetaDataUpdater doesn't throw exceptions.
298 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
299 throw $exception;
300 }
301
302 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
303 // callers should check permissions *before* enqueueing updates. If the main transaction
304 // round actions succeed but some deferred updates fail due to permissions errors then
305 // there is a risk that some secondary data was not properly updated.
306 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
307 throw $guiError;
308 }
309 }
310
349 public static function tryOpportunisticExecute(): bool {
350 // Leave execution up to the current loop if an update is already in progress
351 // or if updates are explicitly disabled
352 if ( self::getRecursiveExecutionStackDepth()
353 || self::$preventOpportunisticUpdates
354 ) {
355 return false;
356 }
357
358 if ( self::getScopeStack()->allowOpportunisticUpdates() ) {
359 self::doUpdates( self::ALL );
360 return true;
361 }
362
363 return false;
364 }
365
370 #[\NoDiscard]
371 public static function preventOpportunisticUpdates(): ScopedCallback {
372 self::$preventOpportunisticUpdates++;
373 return new ScopedCallback( static function () {
374 self::$preventOpportunisticUpdates--;
375 } );
376 }
377
387 public static function pendingUpdatesCount() {
388 return self::getScopeStack()->current()->pendingUpdatesCount();
389 }
390
403 public static function getPendingUpdates( $stage = self::ALL ) {
404 return self::getScopeStack()->current()->getPendingUpdates( $stage );
405 }
406
415 public static function clearPendingUpdates() {
416 self::getScopeStack()->current()->clearPendingUpdates();
417 }
418
425 public static function getRecursiveExecutionStackDepth() {
426 return self::getScopeStack()->getRecursiveDepth();
427 }
428
441 public static function attemptUpdate( DeferrableUpdate $update ) {
442 self::getScopeStack()->onRunUpdateStart( $update );
443
444 $update->doUpdate();
445
446 self::getScopeStack()->onRunUpdateEnd( $update );
447 }
448}
449
451class_alias( DeferredUpdates::class, 'DeferredUpdates' );
wfGetCaller( $level=2)
Get the name of the function which called this function wfGetCaller( 1 ) is the function with the wfG...
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
This class decouples DeferredUpdates's awareness of MediaWikiServices to ease unit testing.
DeferredUpdates helper class for tracking DeferrableUpdate::doUpdate() nesting levels caused by neste...
Defer callable updates to run later in the PHP process.
static getRecursiveExecutionStackDepth()
Get the number of in-progress calls to DeferredUpdates::doUpdates()
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dependeeDbws=[])
Add an update to the pending update queue that invokes the specified callback when run.
static doUpdates( $stage=self::ALL)
Consume and execute all pending updates.
static getPendingUpdates( $stage=self::ALL)
Get a list of the pending updates for the current execution context.
static pendingUpdatesCount()
Get the number of pending updates for the current execution context.
static setScopeStack(DeferredUpdatesScopeStack $scopeStack)
static tryOpportunisticExecute()
Consume and execute pending updates now if possible, instead of waiting.
static attemptUpdate(DeferrableUpdate $update)
Attempt to run an update with the appropriate transaction round state if needed.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
static preventOpportunisticUpdates()
Prevent opportunistic updates until the returned ScopedCallback is consumed.
static clearPendingUpdates()
Cancel all pending updates for the current execution context.
DeferrableUpdate for closure/callable.
An error page which can definitely be safely rendered using the OutputPage.
Handler class for MWExceptions.
Create PSR-3 logger objects.
Callback wrapper that has an originating method.
Interface that deferrable updates should implement.
doUpdate()
Perform the actual work.
Deferrable update that specifies whether it must run outside of any explicit LBFactory transaction ro...
Interface to a relational database.
Definition IDatabase.php:31
LoggerInterface $logger
The logger instance.