MediaWiki  master
DeferredUpdates.php
Go to the documentation of this file.
1 <?php
23 use 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).
PSR-3 logger instance factory.
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