current()->addUpdate( $update, $stage ); self::tryOpportunisticExecute(); } /** * Add an update to the pending update queue that invokes the specified callback when run * * @param callable $callable One of the following: * - A Closure callback that takes the caller name as its argument * - A non-Closure callback that takes no arguments * @param int $stage One of (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND) * @param IDatabase|IDatabase[] $dependeeDbws DB handles which might have pending writes * upon which this update depends. If any of the handles already has an open transaction, * a rollback thereof will cause this update to be cancelled (if it has not already run). * [optional] (since 1.28) * @since 1.27 Added $stage parameter * @since 1.28 Added the $dbw parameter * @since 1.43 Closures are now given the caller name parameter */ public static function addCallableUpdate( $callable, $stage = self::POSTSEND, $dependeeDbws = [] ) { self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dependeeDbws ), $stage ); } /** * Run an update, and, if an error was thrown, catch/log it and enqueue the update as * a job in the job queue system if possible (e.g. implements EnqueueableDataUpdate) * * @param DeferrableUpdate $update * @return Throwable|null */ private static function run( DeferrableUpdate $update ): ?Throwable { $logger = LoggerFactory::getInstance( 'DeferredUpdates' ); $type = get_class( $update ) . ( $update instanceof DeferrableCallback ? '_' . $update->getOrigin() : '' ); $updateId = spl_object_id( $update ); $logger->debug( "DeferredUpdates::run: started $type #{updateId}", [ 'updateId' => $updateId ] ); $updateException = null; $startTime = microtime( true ); try { self::attemptUpdate( $update ); } catch ( Throwable $updateException ) { MWExceptionHandler::logException( $updateException ); $logger->error( "Deferred update '{deferred_type}' failed to run.", [ 'deferred_type' => $type, 'exception' => $updateException, ] ); self::getScopeStack()->onRunUpdateFailed( $update ); } finally { $walltime = microtime( true ) - $startTime; $logger->debug( "DeferredUpdates::run: ended $type #{updateId}, processing time: {walltime}", [ 'updateId' => $updateId, 'walltime' => $walltime, ] ); } // Try to push the update as a job so it can run later if possible if ( $updateException && $update instanceof EnqueueableDataUpdate ) { try { self::getScopeStack()->queueDataUpdate( $update ); } catch ( Throwable $jobException ) { MWExceptionHandler::logException( $jobException ); $logger->error( "Deferred update '{deferred_type}' failed to enqueue as a job.", [ 'deferred_type' => $type, 'exception' => $jobException, ] ); self::getScopeStack()->onRunUpdateFailed( $update ); } } return $updateException; } /** * Consume and execute all pending updates * * Note that it is rarely the case that this method should be called outside of a few * select entry points. For simplicity, that kind of recursion is discouraged. Recursion * cannot happen if an explicit transaction round is active, which limits usage to updates * with TRX_ROUND_ABSENT that do not leave open any transactions round of their own during * the call to this method. * * In the less-common case of this being called within an in-progress DeferrableUpdate, * this will not see any top-queue updates (since they were consumed and are being run * inside an outer execution loop). In that case, it will instead operate on the sub-queue * of the innermost in-progress update on the stack. * * @internal For use by MediaWiki, Maintenance, JobRunner, JobExecutor * @param int $stage Which updates to process. One of * (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND, DeferredUpdates::ALL) */ public static function doUpdates( $stage = self::ALL ) { /** @var ErrorPageError $guiError First presentable client-level error thrown */ $guiError = null; /** @var Throwable $exception First of any error thrown */ $exception = null; $scope = self::getScopeStack()->current(); // T249069: recursion is not possible once explicit transaction rounds are involved $activeUpdate = $scope->getActiveUpdate(); if ( $activeUpdate ) { $class = get_class( $activeUpdate ); if ( !( $activeUpdate instanceof TransactionRoundAwareUpdate ) ) { throw new LogicException( __METHOD__ . ": reached from $class, which is not TransactionRoundAwareUpdate" ); } if ( $activeUpdate->getTransactionRoundRequirement() !== $activeUpdate::TRX_ROUND_ABSENT ) { throw new LogicException( __METHOD__ . ": reached from $class, which does not specify TRX_ROUND_ABSENT" ); } } $scope->processUpdates( $stage, static function ( DeferrableUpdate $update, $activeStage ) use ( &$guiError, &$exception ) { $scopeStack = self::getScopeStack(); $childScope = $scopeStack->descend( $activeStage, $update ); try { $e = self::run( $update ); $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null ); $exception = $exception ?: $e; // Any addUpdate() calls between descend() and ascend() used the sub-queue. // In rare cases, DeferrableUpdate::doUpdates() will process them by calling // doUpdates() itself. In any case, process remaining updates in the subqueue. // them, enqueueing them, or transferring them to the parent scope // queues as appropriate... $childScope->processUpdates( $activeStage, static function ( DeferrableUpdate $sub ) use ( &$guiError, &$exception ) { $e = self::run( $sub ); $guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null ); $exception = $exception ?: $e; } ); } finally { $scopeStack->ascend(); } } ); // VW-style hack to work around T190178, so we can make sure // PageMetaDataUpdater doesn't throw exceptions. if ( $exception && defined( 'MW_PHPUNIT_TEST' ) ) { throw $exception; } // Throw the first of any GUI errors as long as the context is HTTP pre-send. However, // callers should check permissions *before* enqueueing updates. If the main transaction // round actions succeed but some deferred updates fail due to permissions errors then // there is a risk that some secondary data was not properly updated. if ( $guiError && $stage === self::PRESEND && !headers_sent() ) { throw $guiError; } } /** * Consume and execute pending updates now if possible, instead of waiting. * * In web requests, updates are always deferred until the end of the request. * * In CLI mode, updates run earlier and more often. This is important for long-running * Maintenance scripts that would otherwise grow an excessively large queue, which increases * memory use, and risks losing all updates if the script ends early or crashes. * * The folllowing conditions are required for updates to run early in CLI mode: * * - No update is already in progress (ensure linear flow, recursion guard). * - LBFactory indicates that we don't have any "busy" database connections, i.e. * there are no pending writes or otherwise active and uncommitted transactions, * except if the transaction is empty and merely used for primary DB read queries, * in which case the transaction (and its repeatable-read snapshot) can be safely flushed. * * How this works: * * - When a maintenance script commits a change or waits for replication, such as * via IConnectionProvider::commitAndWaitForReplication, then ILBFactory calls * tryOpportunisticExecute(). This is injected via MWLBFactory::applyGlobalState. * * - For maintenance scripts that don't do much with the database, we also call * tryOpportunisticExecute() after every addUpdate() call. * * - Upon the completion of Maintenance::execute() via Maintenance::shutdown(), * any remaining updates are run. * * Note that this method runs both PRESEND and POSTSEND updates and thus should not be called * during web requests. It is only intended for long-running Maintenance scripts. * * @internal For use by Maintenance * @since 1.28 * @return bool Whether updates were allowed to run */ public static function tryOpportunisticExecute(): bool { // Leave execution up to the current loop if an update is already in progress // or if updates are explicitly disabled if ( self::getRecursiveExecutionStackDepth() || self::$preventOpportunisticUpdates ) { return false; } if ( self::getScopeStack()->allowOpportunisticUpdates() ) { self::doUpdates( self::ALL ); return true; } return false; } /** * Prevent opportunistic updates until the returned ScopedCallback is * consumed. * * @return ScopedCallback */ public static function preventOpportunisticUpdates() { self::$preventOpportunisticUpdates++; return new ScopedCallback( static function () { self::$preventOpportunisticUpdates--; } ); } /** * Get the number of pending updates for the current execution context * * If an update is in progress, then this operates on the sub-queues of the * innermost in-progress update. Otherwise, it acts on the top-queues. * * @return int * @since 1.28 */ public static function pendingUpdatesCount() { return self::getScopeStack()->current()->pendingUpdatesCount(); } /** * Get a list of the pending updates for the current execution context * * If an update is in progress, then this operates on the sub-queues of the * innermost in-progress update. Otherwise, it acts on the top-queues. * * @param int $stage Look for updates with this "defer until" stage. One of * (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND, DeferredUpdates::ALL) * @return DeferrableUpdate[] * @internal This method should only be used for unit tests * @since 1.29 */ public static function getPendingUpdates( $stage = self::ALL ) { return self::getScopeStack()->current()->getPendingUpdates( $stage ); } /** * Cancel all pending updates for the current execution context * * If an update is in progress, then this operates on the sub-queues of the * innermost in-progress update. Otherwise, it acts on the top-queues. * * @internal This method should only be used for unit tests */ public static function clearPendingUpdates() { self::getScopeStack()->current()->clearPendingUpdates(); } /** * Get the number of in-progress calls to DeferredUpdates::doUpdates() * * @return int * @internal This method should only be used for unit tests */ public static function getRecursiveExecutionStackDepth() { return self::getScopeStack()->getRecursiveDepth(); } /** * Attempt to run an update with the appropriate transaction round state if needed * * It is allowed for a DeferredUpdate to directly execute one or more other DeferredUpdate * instances without queueing them by calling this method. In that case, the outer update * must use TransactionRoundAwareUpdate::TRX_ROUND_ABSENT, e.g. by extending * TransactionRoundDefiningUpdate, so that this method can give each update its own * transaction round. * * @param DeferrableUpdate $update * @since 1.34 */ public static function attemptUpdate( DeferrableUpdate $update ) { self::getScopeStack()->onRunUpdateStart( $update ); $update->doUpdate(); self::getScopeStack()->onRunUpdateEnd( $update ); } } /** @deprecated class alias since 1.42 */ class_alias( DeferredUpdates::class, 'DeferredUpdates' );