MediaWiki 1.41.2
DeferredUpdates.php
Go to the documentation of this file.
1<?php
23use Wikimedia\ScopedCallback;
24
95 public const ALL = 0;
97 public const PRESEND = 1;
99 public const POSTSEND = 2;
100
102 public const STAGES = [ self::PRESEND, self::POSTSEND ];
103
105 private const BIG_QUEUE_SIZE = 100;
106
108 private static $scopeStack;
109
113 private static $preventOpportunisticUpdates = 0;
114
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
169 public static function addCallableUpdate( $callable, $stage = self::POSTSEND, $dbw = null ) {
170 self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dbw ), $stage );
171 }
172
180 private static function run( DeferrableUpdate $update ): ?Throwable {
181 $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
182
183 $type = get_class( $update )
184 . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' );
185 $updateId = spl_object_id( $update );
186 $logger->debug( __METHOD__ . ": started $type #$updateId" );
187
188 $updateException = null;
189
190 $startTime = microtime( true );
191 try {
192 self::attemptUpdate( $update );
193 } catch ( Throwable $updateException ) {
194 MWExceptionHandler::logException( $updateException );
195 $logger->error(
196 "Deferred update '{deferred_type}' failed to run.",
197 [
198 'deferred_type' => $type,
199 'exception' => $updateException,
200 ]
201 );
202 self::getScopeStack()->onRunUpdateFailed( $update );
203 } finally {
204 $walltime = microtime( true ) - $startTime;
205 $logger->debug( __METHOD__ . ": ended $type #$updateId, processing time: $walltime" );
206 }
207
208 // Try to push the update as a job so it can run later if possible
209 if ( $updateException && $update instanceof EnqueueableDataUpdate ) {
210 try {
211 self::getScopeStack()->queueDataUpdate( $update );
212 } catch ( Throwable $jobException ) {
213 MWExceptionHandler::logException( $jobException );
214 $logger->error(
215 "Deferred update '{deferred_type}' failed to enqueue as a job.",
216 [
217 'deferred_type' => $type,
218 'exception' => $jobException,
219 ]
220 );
221 self::getScopeStack()->onRunUpdateFailed( $update );
222 }
223 }
224
225 return $updateException;
226 }
227
246 public static function doUpdates( $stage = self::ALL ) {
248 $guiError = null;
250 $exception = null;
251
252 $scope = self::getScopeStack()->current();
253
254 // T249069: recursion is not possible once explicit transaction rounds are involved
255 $activeUpdate = $scope->getActiveUpdate();
256 if ( $activeUpdate ) {
257 $class = get_class( $activeUpdate );
258 if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) {
259 throw new LogicException(
260 __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate"
261 );
262 }
263 if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) {
264 throw new LogicException(
265 __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT"
266 );
267 }
268 }
269
270 $scope->processUpdates(
271 $stage,
272 static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) {
273 $scopeStack = self::getScopeStack();
274 $childScope = $scopeStack->descend( $activeStage, $update );
275 try {
276 $e = self::run( $update );
277 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
278 $exception = $exception ?: $e;
279 // Any addUpdate() calls between descend() and ascend() used the sub-queue.
280 // In rare cases, DeferrableUpdate::doUpdates() will process them by calling
281 // doUpdates() itself. In any case, process remaining updates in the subqueue.
282 // them, enqueueing them, or transferring them to the parent scope
283 // queues as appropriate...
284 $childScope->processUpdates(
285 $activeStage,
286 static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) {
287 $e = self::run( $sub );
288 $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
289 $exception = $exception ?: $e;
290 }
291 );
292 } finally {
293 $scopeStack->ascend();
294 }
295 }
296 );
297
298 // VW-style hack to work around T190178, so we can make sure
299 // PageMetaDataUpdater doesn't throw exceptions.
300 if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) {
301 throw $exception;
302 }
303
304 // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
305 // callers should check permissions *before* enqueueing updates. If the main transaction
306 // round actions succeed but some deferred updates fail due to permissions errors then
307 // there is a risk that some secondary data was not properly updated.
308 if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
309 throw $guiError;
310 }
311 }
312
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 if ( self::pendingUpdatesCount() >= self::BIG_QUEUE_SIZE ) {
364 // There are a large number of pending updates and none of them can run yet.
365 // The odds of losing updates due to an error increases when executing long queues
366 // and when large amounts of time pass while tasks are queued. Mitigate this by
367 // trying to eagerly move updates to the JobQueue when possible.
368 //
369 // TODO: Do we still need this now maintenance scripts automatically call
370 // tryOpportunisticExecute from addUpdate, from every commit, and every
371 // waitForReplication call?
372 self::getScopeStack()->current()->consumeMatchingUpdates(
373 self::ALL,
374 EnqueueableDataUpdate::class,
375 static function ( EnqueueableDataUpdate $update ) {
376 self::getScopeStack()->queueDataUpdate( $update );
377 }
378 );
379 }
380
381 return false;
382 }
383
390 public static function preventOpportunisticUpdates() {
391 self::$preventOpportunisticUpdates++;
392 return new ScopedCallback( static function () {
393 self::$preventOpportunisticUpdates--;
394 } );
395 }
396
406 public static function pendingUpdatesCount() {
407 return self::getScopeStack()->current()->pendingUpdatesCount();
408 }
409
422 public static function getPendingUpdates( $stage = self::ALL ) {
423 return self::getScopeStack()->current()->getPendingUpdates( $stage );
424 }
425
434 public static function clearPendingUpdates() {
435 self::getScopeStack()->current()->clearPendingUpdates();
436 }
437
444 public static function getRecursiveExecutionStackDepth() {
445 return self::getScopeStack()->getRecursiveDepth();
446 }
447
460 public static function attemptUpdate( DeferrableUpdate $update ) {
461 self::getScopeStack()->onRunUpdateStart( $update );
462
463 $update->doUpdate();
464
465 self::getScopeStack()->onRunUpdateEnd( $update );
466 }
467}
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:88
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 addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
static tryOpportunisticExecute()
Consume and execute pending updates now if possible, instead of waiting.
static pendingUpdatesCount()
Get the number of pending updates for the current execution context.
static getRecursiveExecutionStackDepth()
Get the number of in-progress calls to DeferredUpdates::doUpdates()
static clearPendingUpdates()
Cancel all pending updates for the current execution context.
static setScopeStack(DeferredUpdatesScopeStack $scopeStack)
static attemptUpdate(DeferrableUpdate $update)
Attempt to run an update with the appropriate transaction round state if needed.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
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 preventOpportunisticUpdates()
Prevent opportunistic updates until the returned ScopedCallback is consumed.
static getPendingUpdates( $stage=self::ALL)
Get a list of the pending updates for the current execution context.
An error page which can definitely be safely rendered using the OutputPage.
DeferrableUpdate for closure/callable.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Create PSR-3 logger objects.
Callback wrapper that has an originating method.
Interface that deferrable updates should implement.
doUpdate()
Perform the actual work.
Interface that marks a DataUpdate as enqueuable via the JobQueue.
Deferrable update that specifies whether it must run outside of any explicit LBFactory transaction ro...
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:36