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
245 public static function doUpdates( $stage = self::ALL ) {
247 $guiError = null;
248 '@phan-var ErrorPageError $guiError';
250 $exception = null;
251 '@phan-var Throwable $exception';
252
253 $scope = self::getScopeStack()->current();
254
255 // T249069: recursion is not possible once explicit transaction rounds are involved
256 $activeUpdate = $scope->getActiveUpdate();
257 if ( $activeUpdate ) {
258 $class = get_class( $activeUpdate );
259 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
260 throw new LogicException(
261 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
262 );
263 }
264 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
265 throw new LogicException(
266 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
267 );
268 }
269 }
270
271 $scope->processUpdates(
272 $stage,
273 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
274 $scopeStack = self::getScopeStack();
275 $childScope = $scopeStack->descend( $activeStage, $update );
276 try {
277 $e = self::run( $update );
278 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
279 $exception = $exception ?: $e;
280 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
281 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
282 // doUpdates() itself. In any case, process remaining updates in the subqueue.
283 // them, enqueuing them, or transferring them to the parent scope
284 // queues as appropriate...
285 $childScope->processUpdates(
286 $activeStage,
287 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
288 $e = self::run( $sub );
289 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
290 $exception = $exception ?: $e;
291 }
292 );
293 } finally {
294 $scopeStack->ascend();
295 }
296 }
297 );
298
299 // VW-style hack to work around T190178, so we can make sure
300 // PageMetaDataUpdater doesn't throw exceptions.
301 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
302 // @phan-suppress-next-line PhanThrowTypeMismatch
303 throw $exception;
304 }
305
306 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
307 // callers should check permissions *before* enqueueing updates. If the main transaction
308 // round actions succeed but some deferred updates fail due to permissions errors then
309 // there is a risk that some secondary data was not properly updated.
310 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
311 throw $guiError;
312 }
313 }
314
353 public static function tryOpportunisticExecute(): bool {
354 // Leave execution up to the current loop if an update is already in progress
355 // or if updates are explicitly disabled
356 if ( self::getRecursiveExecutionStackDepth()
357 || self::$preventOpportunisticUpdates
358 ) {
359 return false;
360 }
361
362 if ( self::getScopeStack()->allowOpportunisticUpdates() ) {
363 self::doUpdates( self::ALL );
364 return true;
365 }
366
367 return false;
368 }
369
374 #[\NoDiscard]
375 public static function preventOpportunisticUpdates(): ScopedCallback {
376 self::$preventOpportunisticUpdates++;
377 return new ScopedCallback( static function () {
378 self::$preventOpportunisticUpdates--;
379 } );
380 }
381
391 public static function pendingUpdatesCount() {
392 return self::getScopeStack()->current()->pendingUpdatesCount();
393 }
394
407 public static function getPendingUpdates( $stage = self::ALL ) {
408 return self::getScopeStack()->current()->getPendingUpdates( $stage );
409 }
410
419 public static function clearPendingUpdates() {
420 self::getScopeStack()->current()->clearPendingUpdates();
421 }
422
429 public static function getRecursiveExecutionStackDepth() {
430 return self::getScopeStack()->getRecursiveDepth();
431 }
432
445 public static function attemptUpdate( DeferrableUpdate $update ) {
446 self::getScopeStack()->onRunUpdateStart( $update );
447
448 $update->doUpdate();
449
450 self::getScopeStack()->onRunUpdateEnd( $update );
451 }
452}
453
455class_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.