2011-09-10 06:50:30 +00:00
|
|
|
<?php
|
2012-05-20 15:56:43 +00:00
|
|
|
/**
|
|
|
|
|
* Interface and manager for deferred updates.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
*/
|
2019-07-11 19:47:48 +00:00
|
|
|
|
2020-01-10 00:00:51 +00:00
|
|
|
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
|
|
|
|
|
use MediaWiki\Logger\LoggerFactory;
|
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2019-07-11 19:47:48 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\Rdbms\DBTransactionError;
|
2017-02-10 18:09:05 +00:00
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
2019-05-21 21:52:57 +00:00
|
|
|
use Wikimedia\Rdbms\ILBFactory;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\Rdbms\LBFactory;
|
2017-02-18 00:26:47 +00:00
|
|
|
use Wikimedia\Rdbms\LoadBalancer;
|
2012-05-20 15:56:43 +00:00
|
|
|
|
2011-09-10 06:50:30 +00:00
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* Class for managing the deferral of updates within the scope of a PHP script invocation
|
2015-05-18 21:20:35 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* In web request mode, deferred updates run at the end of request execution, after the main
|
|
|
|
|
* database transaction round ends, and either before (PRESEND) or after (POSTSEND) the HTTP
|
|
|
|
|
* response has been sent. If an update runs after the HTTP response is sent, it will not block
|
|
|
|
|
* clients. Otherwise, the client will not see the response until the update finishes. Use the
|
|
|
|
|
* PRESEND and POSTSEND class constants to specify when an update should run. POSTSEND is the
|
|
|
|
|
* default for DeferredUpdates::addUpdate() and DeferredUpdates::addCallableUpdate(). An update
|
|
|
|
|
* that might need to alter the HTTP response output must use PRESEND. The control flow with
|
|
|
|
|
* regard to deferred updates during a typical state changing web request is as follows:
|
|
|
|
|
* - 1) Main transaction round starts
|
|
|
|
|
* - 2) Various writes to RBMS/file/blob stores and deferred updates enqueued
|
|
|
|
|
* - 3) Main transaction round ends
|
|
|
|
|
* - 4) PRESEND pending update queue is B1...BN
|
|
|
|
|
* - 5) B1 runs, resulting PRESEND updates iteratively run in FIFO order; likewise for B2..BN
|
|
|
|
|
* - 6) The web response is sent out to the client
|
|
|
|
|
* - 7) POSTSEND pending update queue is A1...AM
|
|
|
|
|
* - 8) A1 runs, resulting updates iteratively run in FIFO order; likewise for A2..AM
|
2016-09-05 03:09:30 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* @see MediaWiki::restInPeace()
|
2015-11-30 22:02:53 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* In CLI mode, no distinction is made between PRESEND and POSTSEND deferred updates and all of
|
|
|
|
|
* them will run during the following occasions:
|
|
|
|
|
* - a) During DeferredUpdates::addUpdate() if no LBFactory DB handles have writes pending
|
2018-05-02 23:56:07 +00:00
|
|
|
* - b) On commit of an LBFactory DB handle if no other such handles have writes pending
|
|
|
|
|
* - c) During an LBFactory::waitForReplication call if no LBFactory DBs have writes pending
|
|
|
|
|
* - d) When the queue is large and an LBFactory DB handle commits (EnqueueableDataUpdate only)
|
2020-05-06 14:37:00 +00:00
|
|
|
* - e) Upon the completion of Maintenance::execute() via Maintenance::shutdown()
|
2018-05-02 23:56:07 +00:00
|
|
|
*
|
rdbms: Move setLBFactoryTriggers from doMaintenance to service wiring
This logic is not needed to run on every PHP process and was making
it difficult to run offline maintenance scripts without additional
complexity based on Maintenance::getDbType and DB_NONE.
Instead of skipping this only for DB_NONE, and establishing a pattern
that may spread to other ad-hoc places throughout the codebase, instead
remove this entirely from the eager set up code for all PHP processes
and move it to the service wiring and dependency injection.
That way, it naturally doesn't happen until and unless the DB service
is actually called upon. Scripts and entry point that need to disable
the DB service, can continue to use
MediaWikiServices::disableStorageBackend.
== Impact on SiteStatsUpdate ==
With wgCommandLineMode no longer being read at run-time from a global,
but in service wiring, this means SiteStatsUpdateTest can't change
the behaviour between CLI-like and Web-like, unless it e.g. resets
the 'DBLoadBalancerFactory' service first. Unfortunately, while most
any reset is supported, a reset of the 'DBLoadBalancerFactory' would
be unsupported as that would lose the temporary db clone context and
such, bringing us to either the developer's live db, or a broken set
up altogether. If there is a strong need for toggling oppertunistic
updates off and on at run-time, this could be supported in the
DeferredUpdates class perhaps, but we already have numerous methods
there (incl db begin/commit being a good proxy already), which this
test already used, so for now I've just removed the extra assertion
for this as it wasn't essential to that test.
Bug: T228895
Bug: T238436
Change-Id: Icf29bc484c155f52b6d8f61e5902233a15ba0c6d
2021-04-15 23:08:44 +00:00
|
|
|
* @see MWLBFactory::applyGlobalState()
|
2015-11-30 22:02:53 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* If DeferredUpdates::doUpdates() is currently running a deferred update, then the public
|
|
|
|
|
* DeferredUpdates interface operates on the PRESEND/POSTSEND "sub"-queues that correspond to
|
|
|
|
|
* the innermost in-progress deferred update. Otherwise, the public interface operates on the
|
|
|
|
|
* PRESEND/POSTSEND "top"-queues. Affected methods include:
|
|
|
|
|
* - DeferredUpdates::addUpdate()
|
|
|
|
|
* - DeferredUpdates::addCallableUpdate()
|
|
|
|
|
* - DeferredUpdates::doUpdates()
|
|
|
|
|
* - DeferredUpdates::tryOpportunisticExecute()
|
|
|
|
|
* - DeferredUpdates::pendingUpdatesCount()
|
|
|
|
|
* - DeferredUpdates::getPendingUpdates()
|
|
|
|
|
* - DeferredUpdates::clearPendingUpdates()
|
|
|
|
|
*
|
|
|
|
|
* Updates that work through this system will be more likely to complete by the time the
|
|
|
|
|
* client makes their next request after this request than with the JobQueue system.
|
2011-09-10 17:16:41 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.19
|
2011-09-10 06:50:30 +00:00
|
|
|
*/
|
|
|
|
|
class DeferredUpdates {
|
2020-05-06 14:37:00 +00:00
|
|
|
/** @var DeferredUpdatesScopeStack|null Queue states based on recursion level */
|
|
|
|
|
private static $scopeStack;
|
|
|
|
|
|
|
|
|
|
/** @var int Process all updates; in web requests, use only after flushing output buffer */
|
|
|
|
|
public const ALL = 0;
|
|
|
|
|
/** @var int Specify/process updates that should run before flushing output buffer */
|
|
|
|
|
public const PRESEND = 1;
|
|
|
|
|
/** @var int Specify/process updates that should run after flushing output buffer */
|
|
|
|
|
public const POSTSEND = 2;
|
2015-11-30 22:02:53 +00:00
|
|
|
|
2020-05-06 14:37:00 +00:00
|
|
|
/** @var int[] List of "defer until" queue stages that can be reached */
|
|
|
|
|
public const STAGES = [ self::PRESEND, self::POSTSEND ];
|
2015-11-30 22:02:53 +00:00
|
|
|
|
2020-05-06 14:37:00 +00:00
|
|
|
/** @var int Queue size threshold for converting updates into jobs */
|
2020-05-12 21:42:08 +00:00
|
|
|
private const BIG_QUEUE_SIZE = 100;
|
2016-08-28 16:06:57 +00:00
|
|
|
|
2011-09-10 06:50:30 +00:00
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* Add an update to the pending update queue for execution at the appropriate time
|
2016-09-01 17:03:31 +00:00
|
|
|
*
|
|
|
|
|
* In CLI mode, callback magic will also be used to run updates when safe
|
2015-11-30 22:02:53 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* If an update is already in progress, then what happens to this update is as follows:
|
2020-05-06 14:37:00 +00:00
|
|
|
* - If it has a "defer until" stage at/before the actual run stage of the innermost
|
|
|
|
|
* in-progress update, then it will go into the sub-queue of that in-progress update.
|
|
|
|
|
* As soon as that update completes, MergeableUpdate instances in its sub-queue will be
|
|
|
|
|
* merged into the top-queues and the non-MergeableUpdate instances will be executed.
|
|
|
|
|
* This is done to better isolate updates from the failures of other updates and reduce
|
|
|
|
|
* the chance of race conditions caused by updates not fully seeing the intended changes
|
|
|
|
|
* of previously enqueued and executed updates.
|
|
|
|
|
* - If it has a "defer until" stage later than the actual run stage of the innermost
|
|
|
|
|
* in-progress update, then it will go into the normal top-queue for that stage.
|
2020-05-06 14:37:00 +00:00
|
|
|
*
|
2013-11-20 18:43:39 +00:00
|
|
|
* @param DeferrableUpdate $update Some object that implements doUpdate()
|
2020-05-06 14:37:00 +00:00
|
|
|
* @param int $stage One of (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND)
|
|
|
|
|
* @since 1.28 Added the $stage parameter
|
2011-09-10 06:50:30 +00:00
|
|
|
*/
|
2016-08-28 16:23:52 +00:00
|
|
|
public static function addUpdate( DeferrableUpdate $update, $stage = self::POSTSEND ) {
|
2016-09-01 17:03:31 +00:00
|
|
|
global $wgCommandLineMode;
|
|
|
|
|
|
2020-05-06 14:37:00 +00:00
|
|
|
self::getScopeStack()->current()->addUpdate( $update, $stage );
|
|
|
|
|
// If CLI mode is active and no RDBMs transaction round is in the way, then run all
|
|
|
|
|
// the pending updates now. This is needed for scripts that never, or rarely, use the
|
|
|
|
|
// RDBMs layer, but that do modify systems via deferred updates. This logic avoids
|
|
|
|
|
// excessive pending update queue sizes when long-running scripts never trigger the
|
|
|
|
|
// basic RDBMs hooks for running pending updates.
|
2016-09-01 17:03:31 +00:00
|
|
|
if ( $wgCommandLineMode ) {
|
|
|
|
|
self::tryOpportunisticExecute( 'run' );
|
|
|
|
|
}
|
2015-11-30 22:02:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* Add an update to the pending update queue that invokes the specified callback when run
|
2015-11-30 22:02:53 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* @see DeferredUpdates::addUpdate()
|
2015-11-30 22:02:53 +00:00
|
|
|
* @see MWCallableUpdate::__construct()
|
|
|
|
|
*
|
|
|
|
|
* @param callable $callable
|
2020-05-06 14:37:00 +00:00
|
|
|
* @param int $stage One of (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND)
|
|
|
|
|
* @param IDatabase|IDatabase[]|null $dbw Abort if this DB is rolled back [optional]
|
|
|
|
|
* @since 1.27 Added $stage parameter
|
|
|
|
|
* @since 1.28 Added the $dbw parameter
|
2015-11-30 22:02:53 +00:00
|
|
|
*/
|
2020-05-06 14:37:00 +00:00
|
|
|
public static function addCallableUpdate( $callable, $stage = self::POSTSEND, $dbw = null ) {
|
2016-08-28 16:23:52 +00:00
|
|
|
self::addUpdate( new MWCallableUpdate( $callable, wfGetCaller(), $dbw ), $stage );
|
2015-11-30 22:02:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* Consume and execute all pending updates
|
2015-11-30 22:02:53 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* 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 an 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.
|
|
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* The $mode parameter determines how the updates are processed. Use "run" to process the
|
|
|
|
|
* updates by running them. Otherwise, use "enqueue" to process the updates by converting
|
|
|
|
|
* the EnqueueableDataUpdate instances to jobs and running the others.
|
2019-03-06 08:04:12 +00:00
|
|
|
*
|
2020-05-06 14:37:00 +00:00
|
|
|
* @param string $mode Either "run" or "enqueue" [default: "run"]
|
|
|
|
|
* @param int $stage Which updates to process. One of
|
|
|
|
|
* (DeferredUpdates::PRESEND, DeferredUpdates::POSTSEND, DeferredUpdates::ALL)
|
|
|
|
|
* @internal For use by MediaWiki, Maintenance, JobRunner, JobExecutor
|
|
|
|
|
* @since 1.27 Added $stage parameter
|
2015-11-30 22:02:53 +00:00
|
|
|
*/
|
2016-08-28 16:23:52 +00:00
|
|
|
public static function doUpdates( $mode = 'run', $stage = self::ALL ) {
|
|
|
|
|
$services = MediaWikiServices::getInstance();
|
|
|
|
|
$stats = $services->getStatsdDataFactory();
|
2019-07-11 19:47:48 +00:00
|
|
|
$lbf = $services->getDBLoadBalancerFactory();
|
|
|
|
|
$logger = LoggerFactory::getInstance( 'DeferredUpdates' );
|
|
|
|
|
$httpMethod = $services->getMainConfig()->get( 'CommandLineMode' )
|
|
|
|
|
? 'cli'
|
|
|
|
|
: strtolower( RequestContext::getMain()->getRequest()->getMethod() );
|
2011-09-10 06:50:30 +00:00
|
|
|
|
2020-05-06 14:37:00 +00:00
|
|
|
/** @var ErrorPageError $guiError First presentable client-level error thrown */
|
|
|
|
|
$guiError = null;
|
|
|
|
|
/** @var Throwable $exception First of any error thrown */
|
2019-07-12 04:38:42 +00:00
|
|
|
$exception = null;
|
|
|
|
|
|
2020-05-06 14:37:00 +00:00
|
|
|
$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"
|
|
|
|
|
);
|
2015-10-08 06:46:18 +00:00
|
|
|
}
|
2020-05-06 14:37:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$scope->processUpdates(
|
|
|
|
|
$stage,
|
|
|
|
|
function ( DeferrableUpdate $update, $activeStage )
|
|
|
|
|
use ( $mode, $lbf, $logger, $stats, $httpMethod, &$guiError, &$exception )
|
|
|
|
|
{
|
|
|
|
|
// If applicable, just enqueue the update as a job in the job queue system
|
|
|
|
|
if ( $mode === 'enqueue' && $update instanceof EnqueueableDataUpdate ) {
|
|
|
|
|
self::jobify( $update, $lbf, $logger, $stats, $httpMethod );
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, run the update....
|
|
|
|
|
$scopeStack = self::getScopeStack();
|
|
|
|
|
$childScope = $scopeStack->descend( $activeStage, $update );
|
|
|
|
|
try {
|
|
|
|
|
$e = self::run( $update, $lbf, $logger, $stats, $httpMethod );
|
|
|
|
|
$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,
|
|
|
|
|
function ( DeferrableUpdate $subUpdate )
|
|
|
|
|
use ( $lbf, $logger, $stats, $httpMethod, &$guiError, &$exception )
|
|
|
|
|
{
|
|
|
|
|
$e = self::run( $subUpdate, $lbf, $logger, $stats, $httpMethod );
|
|
|
|
|
$guiError = $guiError ?: ( $e instanceof ErrorPageError ? $e : null );
|
2019-07-12 04:38:42 +00:00
|
|
|
$exception = $exception ?: $e;
|
2018-03-29 20:14:55 +00:00
|
|
|
}
|
2020-05-06 14:37:00 +00:00
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
$scopeStack->ascend();
|
2012-07-12 21:07:56 +00:00
|
|
|
}
|
2011-09-10 06:50:30 +00:00
|
|
|
}
|
2020-05-06 14:37:00 +00:00
|
|
|
);
|
2016-08-28 16:23:52 +00:00
|
|
|
|
2019-07-12 04:38:42 +00:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-11 19:47:48 +00:00
|
|
|
// 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.
|
2020-05-06 14:37:00 +00:00
|
|
|
if ( $guiError && $stage === self::PRESEND && !headers_sent() ) {
|
|
|
|
|
throw $guiError;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Consume and execute all pending updates unless an update is already
|
|
|
|
|
* in progress or the LBFactory service instance has "busy" DB handles
|
|
|
|
|
*
|
|
|
|
|
* A DB handle is considered "busy" if it has an unfinished transaction that cannot safely
|
|
|
|
|
* be flushed or the parent LBFactory instance has an unfinished transaction round that
|
|
|
|
|
* cannot safely be flushed. If the number of pending updates reaches BIG_QUEUE_SIZE and
|
|
|
|
|
* there are still busy DB handles, then EnqueueableDataUpdate updates might be enqueued
|
|
|
|
|
* as jobs. This avoids excessive memory use and risk of losing updates due to failures.
|
|
|
|
|
*
|
|
|
|
|
* The $mode parameter determines how the updates are processed. Use "run" to process the
|
|
|
|
|
* updates by running them. Otherwise, use "enqueue" to process the updates by converting
|
|
|
|
|
* the EnqueueableDataUpdate instances to jobs and running the others.
|
|
|
|
|
*
|
|
|
|
|
* Note that this method operates on updates from all stages and thus should not be called
|
|
|
|
|
* during web requests. It is only intended for long-running Maintenance scripts.
|
|
|
|
|
*
|
|
|
|
|
* @param string $mode Either "run" or "enqueue" [default: "run"]
|
|
|
|
|
* @return bool Whether updates were allowed to run
|
|
|
|
|
* @internal For use by Maintenance
|
|
|
|
|
* @since 1.28
|
|
|
|
|
*/
|
|
|
|
|
public static function tryOpportunisticExecute( $mode = 'run' ) {
|
|
|
|
|
// Leave execution up to the current loop if an update is already in progress
|
|
|
|
|
if ( self::getRecursiveExecutionStackDepth() ) {
|
|
|
|
|
return false;
|
2016-08-28 16:23:52 +00:00
|
|
|
}
|
2020-05-06 14:37:00 +00:00
|
|
|
|
|
|
|
|
// Run the updates for this context if they will have outer transaction scope
|
|
|
|
|
if ( !self::areDatabaseTransactionsActive() ) {
|
|
|
|
|
self::doUpdates( $mode, self::ALL );
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( self::pendingUpdatesCount() >= self::BIG_QUEUE_SIZE ) {
|
|
|
|
|
// There are a large number of pending updates and none of them can run yet.
|
|
|
|
|
// The odds of losing updates due to an error increase when executing long queues
|
|
|
|
|
// and when large amounts of time pass while tasks are queued. Mitigate this by
|
|
|
|
|
// trying to migrate updates to the job queue system (where applicable).
|
|
|
|
|
self::getScopeStack()->current()->consumeMatchingUpdates(
|
|
|
|
|
self::ALL,
|
|
|
|
|
EnqueueableDataUpdate::class,
|
2021-02-10 22:31:02 +00:00
|
|
|
static function ( EnqueueableDataUpdate $update ) {
|
2020-05-06 14:37:00 +00:00
|
|
|
$spec = $update->getAsJobSpecification();
|
|
|
|
|
JobQueueGroup::singleton( $spec['domain'] )->push( $spec['job'] );
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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();
|
2016-08-28 16:23:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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)
|
2019-05-21 21:52:57 +00:00
|
|
|
*
|
2016-08-28 16:23:52 +00:00
|
|
|
* @param DeferrableUpdate $update
|
2020-05-06 14:37:00 +00:00
|
|
|
* @param ILBFactory $lbFactory
|
2019-07-11 19:47:48 +00:00
|
|
|
* @param LoggerInterface $logger
|
|
|
|
|
* @param StatsdDataFactoryInterface $stats
|
|
|
|
|
* @param string $httpMethod
|
2019-12-29 19:44:43 +00:00
|
|
|
* @return Throwable|null
|
2016-08-28 16:23:52 +00:00
|
|
|
*/
|
2019-07-11 19:47:48 +00:00
|
|
|
private static function run(
|
|
|
|
|
DeferrableUpdate $update,
|
2020-05-06 14:37:00 +00:00
|
|
|
ILBFactory $lbFactory,
|
2019-07-11 19:47:48 +00:00
|
|
|
LoggerInterface $logger,
|
|
|
|
|
StatsdDataFactoryInterface $stats,
|
|
|
|
|
$httpMethod
|
2021-07-22 03:11:47 +00:00
|
|
|
): ?Throwable {
|
2019-07-11 19:47:48 +00:00
|
|
|
$suffix = ( $update instanceof DeferrableCallback ) ? "_{$update->getOrigin()}" : '';
|
2019-07-12 04:38:42 +00:00
|
|
|
$type = get_class( $update ) . $suffix;
|
|
|
|
|
$stats->increment( "deferred_updates.$httpMethod.$type" );
|
2019-07-11 19:47:48 +00:00
|
|
|
|
2020-04-21 22:04:30 +00:00
|
|
|
$updateId = spl_object_id( $update );
|
|
|
|
|
$logger->debug( __METHOD__ . ": started $type #$updateId" );
|
2021-06-09 07:28:38 +00:00
|
|
|
$startTime = microtime( true );
|
2019-07-11 19:47:48 +00:00
|
|
|
$e = null;
|
2016-08-28 16:23:52 +00:00
|
|
|
try {
|
2019-07-11 19:47:48 +00:00
|
|
|
self::attemptUpdate( $update, $lbFactory );
|
2019-07-12 04:38:42 +00:00
|
|
|
|
|
|
|
|
return null;
|
2019-07-11 19:47:48 +00:00
|
|
|
} catch ( Throwable $e ) {
|
2020-04-21 22:04:30 +00:00
|
|
|
} finally {
|
2021-06-09 07:28:38 +00:00
|
|
|
$executionTime = microtime( true ) - $startTime;
|
|
|
|
|
$logger->debug( __METHOD__ . ": ended $type #$updateId, processing time: $executionTime" );
|
2019-07-11 19:47:48 +00:00
|
|
|
}
|
2018-03-29 20:14:55 +00:00
|
|
|
|
2020-03-06 18:28:01 +00:00
|
|
|
MWExceptionHandler::logException( $e );
|
2019-07-12 04:38:42 +00:00
|
|
|
$logger->error(
|
2020-04-21 22:04:30 +00:00
|
|
|
"Deferred update '{deferred_type}' failed to run.",
|
2019-07-12 04:38:42 +00:00
|
|
|
[
|
2019-11-26 20:27:04 +00:00
|
|
|
'deferred_type' => $type,
|
|
|
|
|
'exception' => $e,
|
2019-07-12 04:38:42 +00:00
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
2021-09-02 19:14:52 +00:00
|
|
|
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
|
2019-07-12 04:38:42 +00:00
|
|
|
|
|
|
|
|
// Try to push the update as a job so it can run later if possible
|
|
|
|
|
if ( $update instanceof EnqueueableDataUpdate ) {
|
|
|
|
|
$jobEx = null;
|
|
|
|
|
try {
|
|
|
|
|
$spec = $update->getAsJobSpecification();
|
|
|
|
|
JobQueueGroup::singleton( $spec['domain'] )->push( $spec['job'] );
|
|
|
|
|
|
|
|
|
|
return $e;
|
|
|
|
|
} catch ( Throwable $jobEx ) {
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-06 18:28:01 +00:00
|
|
|
MWExceptionHandler::logException( $jobEx );
|
2019-07-11 19:47:48 +00:00
|
|
|
$logger->error(
|
2020-04-21 22:04:30 +00:00
|
|
|
"Deferred update '{deferred_type}' failed to enqueue as a job.",
|
2019-07-11 19:47:48 +00:00
|
|
|
[
|
2019-11-26 20:27:04 +00:00
|
|
|
'deferred_type' => $type,
|
|
|
|
|
'exception' => $jobEx,
|
2019-07-11 19:47:48 +00:00
|
|
|
]
|
|
|
|
|
);
|
2019-07-12 04:38:42 +00:00
|
|
|
|
2021-09-02 19:14:52 +00:00
|
|
|
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
|
2016-08-28 16:23:52 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-11 19:47:48 +00:00
|
|
|
return $e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-05-06 14:37:00 +00:00
|
|
|
* Enqueue an update as a job in the job queue system and catch/log any exceptions
|
2019-07-11 19:47:48 +00:00
|
|
|
*
|
|
|
|
|
* @param EnqueueableDataUpdate $update
|
|
|
|
|
* @param LBFactory $lbFactory
|
|
|
|
|
* @param LoggerInterface $logger
|
|
|
|
|
* @param StatsdDataFactoryInterface $stats
|
|
|
|
|
* @param string $httpMethod
|
|
|
|
|
*/
|
|
|
|
|
private static function jobify(
|
|
|
|
|
EnqueueableDataUpdate $update,
|
|
|
|
|
LBFactory $lbFactory,
|
|
|
|
|
LoggerInterface $logger,
|
|
|
|
|
StatsdDataFactoryInterface $stats,
|
|
|
|
|
$httpMethod
|
|
|
|
|
) {
|
2019-07-12 04:38:42 +00:00
|
|
|
$type = get_class( $update );
|
|
|
|
|
$stats->increment( "deferred_updates.$httpMethod.$type" );
|
2019-07-11 19:47:48 +00:00
|
|
|
|
2020-03-06 18:28:01 +00:00
|
|
|
$jobEx = null;
|
2019-07-11 19:47:48 +00:00
|
|
|
try {
|
|
|
|
|
$spec = $update->getAsJobSpecification();
|
2019-07-12 04:38:42 +00:00
|
|
|
JobQueueGroup::singleton( $spec['domain'] )->push( $spec['job'] );
|
|
|
|
|
|
|
|
|
|
return;
|
2020-03-06 18:28:01 +00:00
|
|
|
} catch ( Throwable $jobEx ) {
|
2019-07-11 19:47:48 +00:00
|
|
|
}
|
|
|
|
|
|
2020-03-06 18:28:01 +00:00
|
|
|
MWExceptionHandler::logException( $jobEx );
|
2019-07-12 04:38:42 +00:00
|
|
|
$logger->error(
|
2020-03-06 18:28:01 +00:00
|
|
|
"Deferred update '$type' failed to enqueue as a job.",
|
2019-11-26 20:27:04 +00:00
|
|
|
[
|
|
|
|
|
'deferred_type' => $type,
|
2020-03-06 18:28:01 +00:00
|
|
|
'exception' => $jobEx,
|
2019-11-26 20:27:04 +00:00
|
|
|
]
|
2019-07-12 04:38:42 +00:00
|
|
|
);
|
|
|
|
|
|
2021-09-02 19:14:52 +00:00
|
|
|
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
|
2011-09-10 06:50:30 +00:00
|
|
|
}
|
|
|
|
|
|
2019-05-21 21:52:57 +00:00
|
|
|
/**
|
|
|
|
|
* Attempt to run an update with the appropriate transaction round state it expects
|
|
|
|
|
*
|
|
|
|
|
* DeferredUpdate classes that wrap the execution of bundles of other DeferredUpdate
|
|
|
|
|
* instances can use this method to run the updates. Any such wrapper class should
|
|
|
|
|
* always use TRX_ROUND_ABSENT itself.
|
|
|
|
|
*
|
|
|
|
|
* @param DeferrableUpdate $update
|
|
|
|
|
* @param ILBFactory $lbFactory
|
|
|
|
|
* @since 1.34
|
|
|
|
|
*/
|
|
|
|
|
public static function attemptUpdate( DeferrableUpdate $update, ILBFactory $lbFactory ) {
|
2019-07-18 19:27:07 +00:00
|
|
|
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
|
|
|
|
|
if ( !$ticket || $lbFactory->hasTransactionRound() ) {
|
|
|
|
|
throw new DBTransactionError( null, "A database transaction round is pending." );
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 21:07:03 +00:00
|
|
|
if ( $update instanceof DataUpdate ) {
|
2019-07-18 19:27:07 +00:00
|
|
|
$update->setTransactionTicket( $ticket );
|
2019-05-29 21:07:03 +00:00
|
|
|
}
|
|
|
|
|
|
2019-08-02 20:09:44 +00:00
|
|
|
// Designate $update::doUpdate() as the write round owner
|
|
|
|
|
$fnameTrxOwner = ( $update instanceof DeferrableCallback )
|
|
|
|
|
? $update->getOrigin()
|
|
|
|
|
: get_class( $update ) . '::doUpdate';
|
|
|
|
|
// Determine whether the write round will be explicit or implicit
|
2019-07-18 19:27:07 +00:00
|
|
|
$useExplicitTrxRound = !(
|
2019-05-21 21:52:57 +00:00
|
|
|
$update instanceof TransactionRoundAwareUpdate &&
|
|
|
|
|
$update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
|
2019-07-18 19:27:07 +00:00
|
|
|
);
|
2019-08-02 20:09:44 +00:00
|
|
|
|
2019-07-18 19:27:07 +00:00
|
|
|
// Flush any pending changes left over from an implicit transaction round
|
|
|
|
|
if ( $useExplicitTrxRound ) {
|
2021-05-14 22:16:39 +00:00
|
|
|
$lbFactory->beginPrimaryChanges( $fnameTrxOwner ); // new explicit round
|
2019-05-21 21:52:57 +00:00
|
|
|
} else {
|
2021-05-14 22:22:15 +00:00
|
|
|
$lbFactory->commitPrimaryChanges( $fnameTrxOwner ); // new implicit round
|
2019-07-11 19:47:48 +00:00
|
|
|
}
|
2021-09-01 21:04:40 +00:00
|
|
|
// Run the update after any stale primary DB view snapshots have been flushed
|
2019-07-11 19:47:48 +00:00
|
|
|
$update->doUpdate();
|
2019-07-18 19:27:07 +00:00
|
|
|
// Commit any pending changes from the explicit or implicit transaction round
|
2021-05-14 22:22:15 +00:00
|
|
|
$lbFactory->commitPrimaryChanges( $fnameTrxOwner );
|
2019-05-21 21:52:57 +00:00
|
|
|
}
|
|
|
|
|
|
2016-08-28 16:06:57 +00:00
|
|
|
/**
|
2017-06-09 02:56:02 +00:00
|
|
|
* @return bool If a transaction round is active or connection is not ready for commit()
|
2016-08-28 16:06:57 +00:00
|
|
|
*/
|
2017-06-09 02:56:02 +00:00
|
|
|
private static function areDatabaseTransactionsActive() {
|
2016-08-28 16:06:57 +00:00
|
|
|
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
|
2018-05-02 23:56:07 +00:00
|
|
|
if ( $lbFactory->hasTransactionRound() || !$lbFactory->isReadyForRoundOperations() ) {
|
2017-06-09 02:56:02 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$connsBusy = false;
|
2021-02-10 22:31:02 +00:00
|
|
|
$lbFactory->forEachLB( static function ( LoadBalancer $lb ) use ( &$connsBusy ) {
|
2021-09-02 22:54:07 +00:00
|
|
|
$lb->forEachOpenPrimaryConnection( static function ( IDatabase $conn ) use ( &$connsBusy ) {
|
2016-08-28 16:06:57 +00:00
|
|
|
if ( $conn->writesOrCallbacksPending() || $conn->explicitTrxActive() ) {
|
2017-06-09 02:56:02 +00:00
|
|
|
$connsBusy = true;
|
2016-08-28 16:06:57 +00:00
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
return $connsBusy;
|
|
|
|
|
}
|
2020-05-06 14:37:00 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return DeferredUpdatesScopeStack
|
|
|
|
|
*/
|
|
|
|
|
private static function getScopeStack() {
|
|
|
|
|
if ( self::$scopeStack === null ) {
|
|
|
|
|
self::$scopeStack = new DeferredUpdatesScopeStack();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::$scopeStack;
|
|
|
|
|
}
|
2011-09-10 06:50:30 +00:00
|
|
|
}
|