wiki.techinc.nl/includes/deferred/DeferredUpdates.php
Aaron Schulz 34dd248e28 Add pre-send update support to DeferredUpdates
* PRESEND/POSTSEND constants can now be used in addUpdate()
  and addCallableUpdate() to control when the update runs.
  This is useful for updates that may report errors the client
  should see or to just get a head start on queued or pubsub
  based updates like CDN purges. The OutputPage::output() method
  can easily take a few 100ms.
* Removed some argument b/c code from doUpdates().
* Also moved DeferrableUpdate to a separate file.

Change-Id: I9831fe890f9f68f9ad8c4f4bba6921a8f29ba666
2015-12-04 19:08:27 +00:00

181 lines
6.2 KiB
PHP

<?php
/**
* 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
*/
/**
* Class for managing the deferred updates
*
* In web request mode, deferred updates can be run at the end of the request, either before or
* after the HTTP response has been sent. In either case, they run after the DB commit step. If
* an update runs after the response is sent, it will not block clients. If sent before, it will
* run synchronously. If such an update works via queueing, it will be more likely to complete by
* the time the client makes their next request after this one.
*
* In CLI mode, updates are only deferred until the current wiki has no DB write transaction
* active within this request.
*
* When updates are deferred, they use a FIFO queue (one for pre-send and one for post-send).
*
* @since 1.19
*/
class DeferredUpdates {
/** @var DeferrableUpdate[] Updates to be deferred until before request end */
private static $preSendUpdates = array();
/** @var DeferrableUpdate[] Updates to be deferred until after request end */
private static $postSendUpdates = array();
/** @var bool Defer updates fully even in CLI mode */
private static $forceDeferral = false;
const ALL = 0; // all updates
const PRESEND = 1; // for updates that should run before flushing output buffer
const POSTSEND = 2; // for updates that should run after flushing output buffer
/**
* Add an update to the deferred list
*
* @param DeferrableUpdate $update Some object that implements doUpdate()
* @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
*/
public static function addUpdate( DeferrableUpdate $update, $type = self::POSTSEND ) {
if ( $type === self::PRESEND ) {
self::push( self::$preSendUpdates, $update );
} else {
self::push( self::$postSendUpdates, $update );
}
}
/**
* Add a callable update. In a lot of cases, we just need a callback/closure,
* defining a new DeferrableUpdate object is not necessary
*
* @see MWCallableUpdate::__construct()
*
* @param callable $callable
* @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
*/
public static function addCallableUpdate( $callable, $type = self::POSTSEND ) {
self::addUpdate( new MWCallableUpdate( $callable ), $type );
}
/**
* Do any deferred updates and clear the list
*
* @param string $mode Use "enqueue" to use the job queue when possible [Default: "run"]
* @param integer $type DeferredUpdates constant (PRESEND, POSTSEND, or ALL) (since 1.27)
*/
public static function doUpdates( $mode = 'run', $type = self::ALL ) {
if ( $type === self::ALL || $type == self::PRESEND ) {
self::execute( self::$preSendUpdates, $mode );
}
if ( $type === self::ALL || $type == self::POSTSEND ) {
self::execute( self::$postSendUpdates, $mode );
}
}
private static function push( array &$queue, DeferrableUpdate $update ) {
global $wgCommandLineMode;
array_push( $queue, $update );
if ( self::$forceDeferral ) {
return; // do not run
}
// CLI scripts may forget to periodically flush these updates,
// so try to handle that rather than OOMing and losing them entirely.
// Try to run the updates as soon as there is no current wiki transaction.
static $waitingOnTrx = false; // de-duplicate callback
if ( $wgCommandLineMode && !$waitingOnTrx ) {
$lb = wfGetLB();
$dbw = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
// Do the update as soon as there is no transaction
if ( $dbw && $dbw->trxLevel() ) {
$waitingOnTrx = true;
$dbw->onTransactionIdle( function() use ( &$waitingOnTrx ) {
DeferredUpdates::doUpdates();
$waitingOnTrx = false;
} );
} else {
self::doUpdates();
}
}
}
public static function execute( array &$queue, $mode ) {
$updates = $queue; // snapshot of queue
// Keep doing rounds of updates until none get enqueued
while ( count( $updates ) ) {
$queue = array(); // clear the queue
/** @var DataUpdate[] $dataUpdates */
$dataUpdates = array();
/** @var DeferrableUpdate[] $otherUpdates */
$otherUpdates = array();
foreach ( $updates as $update ) {
if ( $update instanceof DataUpdate ) {
$dataUpdates[] = $update;
} else {
$otherUpdates[] = $update;
}
}
// Delegate DataUpdate execution to the DataUpdate class
DataUpdate::runUpdates( $dataUpdates, $mode );
// Execute the non-DataUpdate tasks
foreach ( $otherUpdates as $update ) {
try {
$update->doUpdate();
wfGetLBFactory()->commitMasterChanges();
} catch ( Exception $e ) {
// We don't want exceptions thrown during deferred updates to
// be reported to the user since the output is already sent
if ( !$e instanceof ErrorPageError ) {
MWExceptionHandler::logException( $e );
}
// Make sure incomplete transactions are not committed and end any
// open atomic sections so that other DB updates have a chance to run
wfGetLBFactory()->rollbackMasterChanges();
}
}
$updates = $queue; // new snapshot of queue (check for new entries)
}
}
/**
* Clear all pending updates without performing them. Generally, you don't
* want or need to call this. Unit tests need it though.
*/
public static function clearPendingUpdates() {
self::$preSendUpdates = array();
self::$postSendUpdates = array();
}
/**
* @note This method is intended for testing purposes
* @param bool $value Whether to *always* defer updates, even in CLI mode
* @since 1.27
*/
public static function forceDeferral( $value ) {
self::$forceDeferral = $value;
}
}