MediaWiki master
DeferredUpdates.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Deferred;
8
9use LogicException;
13use Throwable;
15use Wikimedia\ScopedCallback;
16use Wikimedia\Timestamp\ConvertibleTimestamp;
17
88 public const ALL = 0;
90 public const PRESEND = 1;
92 public const POSTSEND = 2;
93
95 public const STAGES = [ self::PRESEND, self::POSTSEND ];
96
98 private static $scopeStack;
99
103 private static $preventOpportunisticUpdates = 0;
104
105 private static function getScopeStack(): DeferredUpdatesScopeStack {
106 self::$scopeStack ??= new DeferredUpdatesScopeMediaWikiStack();
107 return self::$scopeStack;
108 }
109
114 public static function setScopeStack( DeferredUpdatesScopeStack $scopeStack ): void {
115 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
116 throw new LogicException( 'Cannot reconfigure DeferredUpdates outside tests' );
117 }
118 self::$scopeStack = $scopeStack;
119 }
120
141 public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
142 self::getScopeStack()->current()->addUpdate( $update, $stage );
143 self::tryOpportunisticExecute();
144 }
145
161 public static function addCallableUpdate(
162 $callable,
163 $stage = self::POSTSEND,
164 $dependeeDbws = []
165 ) {
166 self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dependeeDbws ), $stage );
167 }
168
176 private static function run( DeferrableUpdate $update ): ?Throwable {
177 $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
178
179 $type = get_class( $update )
180 . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' );
181 $updateId = spl_object_id( $update );
182 $logger->debug( "DeferredUpdates::run: started $type #{updateId}", [ 'updateId' => $updateId ] );
183
184 $updateException = null;
185
186 $startTime = ConvertibleTimestamp::hrtime();
187 try {
188 self::attemptUpdate( $update );
189 } catch ( Throwable $updateException ) {
190 MWExceptionHandler::logException( $updateException );
191 $logger->error(
192 "Deferred update '{deferred_type}' failed to run.",
193 [
194 'deferred_type' => $type,
195 'exception' => $updateException,
196 ]
197 );
198 self::getScopeStack()->onRunUpdateFailed( $update );
199 } finally {
200 $walltime = ( ConvertibleTimestamp::hrtime() - $startTime ) / 1e9;
201 $logger->debug( "DeferredUpdates::run: ended $type #{updateId}, processing time: {walltime}", [
202 'updateId' => $updateId,
203 'walltime' => $walltime,
204 ] );
205 }
206
207 // Try to push the update as a job so it can run later if possible
208 if ( $updateException && $update instanceof EnqueueableDataUpdate ) {
209 try {
210 self::getScopeStack()->queueDataUpdate( $update );
211 } catch ( Throwable $jobException ) {
212 MWExceptionHandler::logException( $jobException );
213 $logger->error(
214 "Deferred update '{deferred_type}' failed to enqueue as a job.",
215 [
216 'deferred_type' => $type,
217 'exception' => $jobException,
218 ]
219 );
220 self::getScopeStack()->onRunUpdateFailed( $update );
221 }
222 }
223
224 return $updateException;
225 }
226
246 public static function doUpdates( $stage = self::ALL ) {
248 $guiError = null;
249 '@phan-var ErrorPageError $guiError';
251 $exception = null;
252 '@phan-var Throwable $exception';
253
254 $scope = self::getScopeStack()->current();
255
256 // T249069: recursion is not possible once explicit transaction rounds are involved
257 $activeUpdate = $scope->getActiveUpdate();
258 if ( $activeUpdate ) {
259 $class = get_class( $activeUpdate );
260 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
261 throw new LogicException(
262 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
263 );
264 }
265 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
266 throw new LogicException(
267 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
268 );
269 }
270 }
271
272 $scope->processUpdates(
273 $stage,
274 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
275 $scopeStack = self::getScopeStack();
276 $childScope = $scopeStack->descend( $activeStage, $update );
277 try {
278 $e = self::run( $update );
279 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
280 $exception = $exception ?: $e;
281 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
282 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
283 // doUpdates() itself. In any case, process remaining updates in the subqueue.
284 // them, enqueuing them, or transferring them to the parent scope
285 // queues as appropriate...
286 $childScope->processUpdates(
287 $activeStage,
288 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
289 $e = self::run( $sub );
290 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
291 $exception = $exception ?: $e;
292 }
293 );
294 } finally {
295 $scopeStack->ascend();
296 }
297 }
298 );
299
300 // VW-style hack to work around T190178, so we can make sure
301 // PageMetaDataUpdater doesn't throw exceptions.
302 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
303 // @phan-suppress-next-line PhanThrowTypeMismatch
304 throw $exception;
305 }
306
307 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
308 // callers should check permissions *before* enqueueing updates. If the main transaction
309 // round actions succeed but some deferred updates fail due to permissions errors then
310 // there is a risk that some secondary data was not properly updated.
311 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
312 throw $guiError;
313 }
314 }
315
354 public static function tryOpportunisticExecute(): bool {
355 // Leave execution up to the current loop if an update is already in progress
356 // or if updates are explicitly disabled
357 if ( self::getRecursiveExecutionStackDepth()
358 || self::$preventOpportunisticUpdates
359 ) {
360 return false;
361 }
362
363 if ( self::getScopeStack()->allowOpportunisticUpdates() ) {
364 self::doUpdates( self::ALL );
365 return true;
366 }
367
368 return false;
369 }
370
375 #[\NoDiscard]
376 public static function preventOpportunisticUpdates(): ScopedCallback {
377 self::$preventOpportunisticUpdates++;
378 return new ScopedCallback( static function () {
379 self::$preventOpportunisticUpdates--;
380 } );
381 }
382
392 public static function pendingUpdatesCount() {
393 return self::getScopeStack()->current()->pendingUpdatesCount();
394 }
395
408 public static function getPendingUpdates( $stage = self::ALL ) {
409 return self::getScopeStack()->current()->getPendingUpdates( $stage );
410 }
411
420 public static function clearPendingUpdates() {
421 self::getScopeStack()->current()->clearPendingUpdates();
422 }
423
430 public static function getRecursiveExecutionStackDepth() {
431 return self::getScopeStack()->getRecursiveDepth();
432 }
433
446 public static function attemptUpdate( DeferrableUpdate $update ) {
447 self::getScopeStack()->onRunUpdateStart( $update );
448
449 $update->doUpdate();
450
451 self::getScopeStack()->onRunUpdateEnd( $update );
452 }
453}
454
456class_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:71
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