MediaWiki master
DeferredUpdates.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Deferred;
22
24use LogicException;
27use Throwable;
29use Wikimedia\ScopedCallback;
30
101 public const ALL = 0;
103 public const PRESEND = 1;
105 public const POSTSEND = 2;
106
108 public const STAGES = [ self::PRESEND, self::POSTSEND ];
109
111 private static $scopeStack;
112
116 private static $preventOpportunisticUpdates = 0;
117
121 private static function getScopeStack(): DeferredUpdatesScopeStack {
122 self::$scopeStack ??= new DeferredUpdatesScopeMediaWikiStack();
123 return self::$scopeStack;
124 }
125
130 public static function setScopeStack( DeferredUpdatesScopeStack $scopeStack ): void {
131 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
132 throw new LogicException( 'Cannot reconfigure DeferredUpdates outside tests' );
133 }
134 self::$scopeStack = $scopeStack;
135 }
136
157 public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
158 self::getScopeStack()->current()->addUpdate( $update, $stage );
159 self::tryOpportunisticExecute();
160 }
161
177 public static function addCallableUpdate(
178 $callable,
179 $stage = self::POSTSEND,
180 $dependeeDbws = []
181 ) {
182 self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dependeeDbws ), $stage );
183 }
184
192 private static function run( DeferrableUpdate $update ): ?Throwable {
193 $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
194
195 $type = get_class( $update )
196 . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' );
197 $updateId = spl_object_id( $update );
198 $logger->debug( "DeferredUpdates::run: started $type #{updateId}", [ 'updateId' => $updateId ] );
199
200 $updateException = null;
201
202 $startTime = microtime( true );
203 try {
204 self::attemptUpdate( $update );
205 } catch ( Throwable $updateException ) {
206 MWExceptionHandler::logException( $updateException );
207 $logger->error(
208 "Deferred update '{deferred_type}' failed to run.",
209 [
210 'deferred_type' => $type,
211 'exception' => $updateException,
212 ]
213 );
214 self::getScopeStack()->onRunUpdateFailed( $update );
215 } finally {
216 $walltime = microtime( true ) - $startTime;
217 $logger->debug( "DeferredUpdates::run: ended $type #{updateId}, processing time: {walltime}", [
218 'updateId' => $updateId,
219 'walltime' => $walltime,
220 ] );
221 }
222
223 // Try to push the update as a job so it can run later if possible
224 if ( $updateException && $update instanceof EnqueueableDataUpdate ) {
225 try {
226 self::getScopeStack()->queueDataUpdate( $update );
227 } catch ( Throwable $jobException ) {
228 MWExceptionHandler::logException( $jobException );
229 $logger->error(
230 "Deferred update '{deferred_type}' failed to enqueue as a job.",
231 [
232 'deferred_type' => $type,
233 'exception' => $jobException,
234 ]
235 );
236 self::getScopeStack()->onRunUpdateFailed( $update );
237 }
238 }
239
240 return $updateException;
241 }
242
261 public static function doUpdates( $stage = self::ALL ) {
263 $guiError = null;
265 $exception = null;
266
267 $scope = self::getScopeStack()->current();
268
269 // T249069: recursion is not possible once explicit transaction rounds are involved
270 $activeUpdate = $scope->getActiveUpdate();
271 if ( $activeUpdate ) {
272 $class = get_class( $activeUpdate );
273 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
274 throw new LogicException(
275 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
276 );
277 }
278 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
279 throw new LogicException(
280 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
281 );
282 }
283 }
284
285 $scope->processUpdates(
286 $stage,
287 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
288 $scopeStack = self::getScopeStack();
289 $childScope = $scopeStack->descend( $activeStage, $update );
290 try {
291 $e = self::run( $update );
292 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
293 $exception = $exception ?: $e;
294 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
295 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
296 // doUpdates() itself. In any case, process remaining updates in the subqueue.
297 // them, enqueueing them, or transferring them to the parent scope
298 // queues as appropriate...
299 $childScope->processUpdates(
300 $activeStage,
301 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
302 $e = self::run( $sub );
303 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
304 $exception = $exception ?: $e;
305 }
306 );
307 } finally {
308 $scopeStack->ascend();
309 }
310 }
311 );
312
313 // VW-style hack to work around T190178, so we can make sure
314 // PageMetaDataUpdater doesn't throw exceptions.
315 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
316 throw $exception;
317 }
318
319 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
320 // callers should check permissions *before* enqueueing updates. If the main transaction
321 // round actions succeed but some deferred updates fail due to permissions errors then
322 // there is a risk that some secondary data was not properly updated.
323 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
324 throw $guiError;
325 }
326 }
327
364 public static function tryOpportunisticExecute(): bool {
365 // Leave execution up to the current loop if an update is already in progress
366 // or if updates are explicitly disabled
367 if ( self::getRecursiveExecutionStackDepth()
368 || self::$preventOpportunisticUpdates
369 ) {
370 return false;
371 }
372
373 if ( self::getScopeStack()->allowOpportunisticUpdates() ) {
374 self::doUpdates( self::ALL );
375 return true;
376 }
377
378 return false;
379 }
380
387 public static function preventOpportunisticUpdates() {
388 self::$preventOpportunisticUpdates++;
389 return new ScopedCallback( static function () {
390 self::$preventOpportunisticUpdates--;
391 } );
392 }
393
403 public static function pendingUpdatesCount() {
404 return self::getScopeStack()->current()->pendingUpdatesCount();
405 }
406
419 public static function getPendingUpdates( $stage = self::ALL ) {
420 return self::getScopeStack()->current()->getPendingUpdates( $stage );
421 }
422
431 public static function clearPendingUpdates() {
432 self::getScopeStack()->current()->clearPendingUpdates();
433 }
434
441 public static function getRecursiveExecutionStackDepth() {
442 return self::getScopeStack()->getRecursiveDepth();
443 }
444
457 public static function attemptUpdate( DeferrableUpdate $update ) {
458 self::getScopeStack()->onRunUpdateStart( $update );
459
460 $update->doUpdate();
461
462 self::getScopeStack()->onRunUpdateEnd( $update );
463 }
464}
465
467class_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...
run()
Run the job.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
An error page which can definitely be safely rendered using the OutputPage.
Handler class for MWExceptions.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
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.
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:48