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
118 private static function getScopeStack(): DeferredUpdatesScopeStack {
119 self::$scopeStack ??= new DeferredUpdatesScopeMediaWikiStack();
120 return self::$scopeStack;
121 }
122
127 public static function setScopeStack( DeferredUpdatesScopeStack $scopeStack ): void {
128 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
129 throw new LogicException( 'Cannot reconfigure DeferredUpdates outside tests' );
130 }
131 self::$scopeStack = $scopeStack;
132 }
133
154 public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
155 self::getScopeStack()->current()->addUpdate( $update, $stage );
156 self::tryOpportunisticExecute();
157 }
158
174 public static function addCallableUpdate(
175 $callable,
176 $stage = self::POSTSEND,
177 $dependeeDbws = []
178 ) {
179 self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dependeeDbws ), $stage );
180 }
181
189 private static function run( DeferrableUpdate $update ): ?Throwable {
190 $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
191
192 $type = get_class( $update )
193 . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' );
194 $updateId = spl_object_id( $update );
195 $logger->debug( "DeferredUpdates::run: started $type #{updateId}", [ 'updateId' => $updateId ] );
196
197 $updateException = null;
198
199 $startTime = microtime( true );
200 try {
201 self::attemptUpdate( $update );
202 } catch ( Throwable $updateException ) {
203 MWExceptionHandler::logException( $updateException );
204 $logger->error(
205 "Deferred update '{deferred_type}' failed to run.",
206 [
207 'deferred_type' => $type,
208 'exception' => $updateException,
209 ]
210 );
211 self::getScopeStack()->onRunUpdateFailed( $update );
212 } finally {
213 $walltime = microtime( true ) - $startTime;
214 $logger->debug( "DeferredUpdates::run: ended $type #{updateId}, processing time: {walltime}", [
215 'updateId' => $updateId,
216 'walltime' => $walltime,
217 ] );
218 }
219
220 // Try to push the update as a job so it can run later if possible
221 if ( $updateException && $update instanceof EnqueueableDataUpdate ) {
222 try {
223 self::getScopeStack()->queueDataUpdate( $update );
224 } catch ( Throwable $jobException ) {
225 MWExceptionHandler::logException( $jobException );
226 $logger->error(
227 "Deferred update '{deferred_type}' failed to enqueue as a job.",
228 [
229 'deferred_type' => $type,
230 'exception' => $jobException,
231 ]
232 );
233 self::getScopeStack()->onRunUpdateFailed( $update );
234 }
235 }
236
237 return $updateException;
238 }
239
258 public static function doUpdates( $stage = self::ALL ) {
260 $guiError = null;
262 $exception = null;
263
264 $scope = self::getScopeStack()->current();
265
266 // T249069: recursion is not possible once explicit transaction rounds are involved
267 $activeUpdate = $scope->getActiveUpdate();
268 if ( $activeUpdate ) {
269 $class = get_class( $activeUpdate );
270 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
271 throw new LogicException(
272 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
273 );
274 }
275 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
276 throw new LogicException(
277 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
278 );
279 }
280 }
281
282 $scope->processUpdates(
283 $stage,
284 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
285 $scopeStack = self::getScopeStack();
286 $childScope = $scopeStack->descend( $activeStage, $update );
287 try {
288 $e = self::run( $update );
289 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
290 $exception = $exception ?: $e;
291 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
292 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
293 // doUpdates() itself. In any case, process remaining updates in the subqueue.
294 // them, enqueueing them, or transferring them to the parent scope
295 // queues as appropriate...
296 $childScope->processUpdates(
297 $activeStage,
298 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
299 $e = self::run( $sub );
300 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
301 $exception = $exception ?: $e;
302 }
303 );
304 } finally {
305 $scopeStack->ascend();
306 }
307 }
308 );
309
310 // VW-style hack to work around T190178, so we can make sure
311 // PageMetaDataUpdater doesn't throw exceptions.
312 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
313 throw $exception;
314 }
315
316 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
317 // callers should check permissions *before* enqueueing updates. If the main transaction
318 // round actions succeed but some deferred updates fail due to permissions errors then
319 // there is a risk that some secondary data was not properly updated.
320 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
321 throw $guiError;
322 }
323 }
324
363 public static function tryOpportunisticExecute(): bool {
364 // Leave execution up to the current loop if an update is already in progress
365 // or if updates are explicitly disabled
366 if ( self::getRecursiveExecutionStackDepth()
367 || self::$preventOpportunisticUpdates
368 ) {
369 return false;
370 }
371
372 if ( self::getScopeStack()->allowOpportunisticUpdates() ) {
373 self::doUpdates( self::ALL );
374 return true;
375 }
376
377 return false;
378 }
379
386 public static function preventOpportunisticUpdates() {
387 self::$preventOpportunisticUpdates++;
388 return new ScopedCallback( static function () {
389 self::$preventOpportunisticUpdates--;
390 } );
391 }
392
402 public static function pendingUpdatesCount() {
403 return self::getScopeStack()->current()->pendingUpdatesCount();
404 }
405
418 public static function getPendingUpdates( $stage = self::ALL ) {
419 return self::getScopeStack()->current()->getPendingUpdates( $stage );
420 }
421
430 public static function clearPendingUpdates() {
431 self::getScopeStack()->current()->clearPendingUpdates();
432 }
433
440 public static function getRecursiveExecutionStackDepth() {
441 return self::getScopeStack()->getRecursiveDepth();
442 }
443
456 public static function attemptUpdate( DeferrableUpdate $update ) {
457 self::getScopeStack()->onRunUpdateStart( $update );
458
459 $update->doUpdate();
460
461 self::getScopeStack()->onRunUpdateEnd( $update );
462 }
463}
464
466class_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:45