* $wgDataCenterId and $wgDataCenterRoles where added, which will serve as
basic configuration settings needed for multi-datacenter setups.
$wgDataCenterUpdateStickTTL was also added.
+* Added a new hook, 'UserMailerTransformContent', to transform the contents
+ of an email. This is similar to the EmailUser hook but applies to all mail
+ sent via UserMailer.
+* Added a new hook, 'UserMailerTransformMessage', to transform the contents
+ of an emai after MIME encoding.
+* Added a new hook, 'UserMailerSplitTo', to control which users have to be
+ emailed separately (ie. there is a single address in the To: field) so
+ user-specific changes to the email can be applied safely.
+* $wgCdnMaxageLagged was added, which limits the CDN cache TTL
+ when any load balancer uses a DB that is lagged beyond the 'max lag'
+ setting in the relevant section of $wgLBFactoryConf.
==== External libraries ====
'DBLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php',
'DBMasterPos' => __DIR__ . '/includes/db/DatabaseUtility.php',
'DBQueryError' => __DIR__ . '/includes/db/DatabaseError.php',
+ 'DBReadOnlyError' => __DIR__ . '/includes/db/DatabaseError.php',
'DBSiteStore' => __DIR__ . '/includes/site/DBSiteStore.php',
'DBUnexpectedError' => __DIR__ . '/includes/db/DatabaseError.php',
'DataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedBagOStuff.php',
'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
'MemcachedPhpBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPhpBagOStuff.php',
+ 'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php',
'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php',
$to: Array of MailAddress objects for the recipients
&$returnPath: The return address string
+'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance
+to split up an email with multiple the To: field into separate emails.
+$to: array of MailAddress objects; unset the ones which should be mailed separately
+
+'UserMailerTransformContent': Called in UserMailer::send() to change email contents.
+Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys
+&$error: should be set to an error message string
+
+'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through
+the MIME transform. Extensions can block sending the email by returning false and setting $error.
+$to: array of MailAdresses of the targets
+$from: MailAddress of the sender
+&$subject: email subject (not MIME encoded)
+&$headers: email headers (except To: and Subject:) as an array of header name => value pairs
+&$body: email body (in MIME format) as a string
+&$error: should be set to an error message string
+
'UserRemoveGroup': Called when removing a group; return false to override stock
group removal.
$user: the user object that is to have a group removed
/** @var string "AND" or "OR" */
protected $mode;
- /** @var DatabaseBase Read-DB slave */
+ /** @var IDatabase Read-DB slave */
protected $dbr;
/**
$wgInternalServer = false;
/**
- * Cache timeout for the squid, will be sent as s-maxage (without ESI) or
- * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in
- * the Squid config.
+ * Cache TTL for the CDN sent as s-maxage (without ESI) or
+ * Surrogate-Control (with ESI). Without ESI, you should strip
+ * out s-maxage in the Squid config.
*
-* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
+ * 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days.
*/
$wgSquidMaxage = 18000;
+/**
+ * Cache timeout for the CDN when DB slave lag is high
+ * @see $wgSquidMaxage
+ * @since 1.27
+ */
+$wgCdnMaxageLagged = 30;
+
/**
* Default maximum age for raw CSS/JS accesses
*
}
/**
- * Make an array to be used for calls to DatabaseBase::buildLike(), which
+ * Make an array to be used for calls to Database::buildLike(), which
* will match the specified string. There are several kinds of filter entry:
* *.domain.com - Produces http://com.domain.%, matches domain.com
* and www.domain.com
*
* @param string $filterEntry Domainparts
* @param string $protocol Protocol (default http://)
- * @return array Array to be passed to DatabaseBase::buildLike() or false on error
+ * @return array Array to be passed to Database::buildLike() or false on error
*/
public static function makeLikeArray( $filterEntry, $protocol = 'http://' ) {
$db = wfGetDB( DB_SLAVE );
$expires = time() + $this->config->get( 'DataCenterUpdateStickTTL' );
$request->response()->setCookie( 'UseDC', 'master', $expires );
}
+
+ // Avoid letting a few seconds of slave lag cause a month of stale data
+ if ( $factory->laggedSlaveUsed() ) {
+ $maxAge = $this->config->get( 'CdnMaxageLagged' );
+ $this->context->getOutput()->lowerCdnMaxage( $maxAge );
+ wfDebugLog( 'replication', "Lagged DB used; CDN cache TTL limited to $maxAge seconds" );
+ }
}
/**
/** @var int Cache stuff. Looks like mEnableClientCache */
protected $mSquidMaxage = 0;
+ /** @var in Upper limit on mSquidMaxage */
+ protected $mCdnMaxageLimit = INF;
/**
* @var bool Controls if anti-clickjacking / frame-breaking headers will
* @param int $maxage Maximum cache time on the Squid, in seconds.
*/
public function setSquidMaxage( $maxage ) {
- $this->mSquidMaxage = $maxage;
+ $this->mSquidMaxage = min( $maxage, $this->mCdnMaxageLimit );
+ }
+
+ /**
+ * Lower the value of the "s-maxage" part of the "Cache-control" HTTP header
+ *
+ * @param int $maxage Maximum cache time on the Squid, in seconds
+ * @since 1.27
+ */
+ public function lowerCdnMaxage( $maxage ) {
+ $this->mCdnMaxageLimit = $this->min( $maxage, $this->mCdnMaxageLimit );
+ $this->setSquidMaxage( $this->mSquidMaxage );
}
/**
/**
* Do the DB query to iterate through the objects.
- * @param IDatabase $db DatabaseBase object to use for the query
+ * @param IDatabase $db DB object to use for the query
*/
abstract public function doQuery( $db );
/**
* Constructor
- * @param bool|DatabaseBase $database
- * - Boolean: whether to use the master DB
- * - DatabaseBase: database connection to use
+ * @param bool|IDatabase $database
+ * - boolean: Whether to use the master DB
+ * - IDatabase: Database connection to use
*/
public function __construct( $database = false ) {
- if ( $database instanceof DatabaseBase ) {
+ if ( $database instanceof IDatabase ) {
$this->db = $database;
} else {
$this->db = wfGetDB( $database ? DB_MASTER : DB_SLAVE );
* for the original initStats, but without output.
*
* @param IDatabase|bool $database
- * - Boolean: whether to use the master DB
- * - DatabaseBase: database connection to use
+ * - boolean: Whether to use the master DB
+ * - IDatabase: Database connection to use
* @param array $options Array of options, may contain the following values
- * - activeUsers Boolean: whether to update the number of active users (default: false)
+ * - activeUsers boolean: Whether to update the number of active users (default: false)
*/
public static function doAllAndCommit( $database, array $options = array() ) {
$options += array( 'update' => false, 'activeUsers' => false );
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
- $u = new HTMLCacheUpdate( $this, 'pagelinks' );
- $u->doUpdate();
-
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
if ( $this->getNamespace() == NS_CATEGORY ) {
- $u = new HTMLCacheUpdate( $this, 'categorylinks' );
- $u->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
}
}
$data['mVersion'] = self::VERSION;
$key = wfMemcKey( 'user', 'id', $this->mId );
- $opts = DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $opts = Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
ObjectCache::getMainWANInstance()->set( $key, $data, 3600, $opts );
}
*
* @param string $database
* @param bool $ignoreInvalidDB If true, don't check if $database is in $wgLocalDatabases
- * @return DatabaseBase|null If invalid selection
+ * @return IDatabase|null If invalid selection
*/
public static function getDB( $database, $ignoreInvalidDB = false ) {
global $wgDBname;
$dbr = wfGetDB( DB_SLAVE );
$dbrWatchlist = wfGetDB( DB_SLAVE, 'watchlist' );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr, $dbrWatchlist );
+ $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist );
$result = array();
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'active-tags' ),
function ( $oldValue, &$ttl, array &$setOpts ) {
- $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
// Ask extensions which tags they consider active
$extensionActive = array();
function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$tags = $dbr->selectFieldValues( 'valid_tag', 'vt_tag', array(), $fname );
return ObjectCache::getMainWANInstance()->getWithSetCallback(
wfMemcKey( 'valid-tags-hook' ),
function ( $oldValue, &$ttl, array &$setOpts ) {
- $setOpts += DatabaseBase::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
+ $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
$tags = array();
Hooks::run( 'ListDefinedTags', array( &$tags ) );
function ( $oldValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_SLAVE, 'vslow' );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$res = $dbr->select(
'change_tag',
$isWriteQuery = $this->isWriteQuery( $sql );
if ( $isWriteQuery ) {
- if ( !$this->mDoneWrites ) {
- wfDebug( __METHOD__ . ': Writes done: ' .
- DatabaseBase::generalizeSQL( $sql ) . "\n" );
+ $reason = $this->getLBInfo( 'readOnlyReason' );
+ if ( is_string( $reason ) ) {
+ throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Set a flag indicating that writes have been done
$this->mDoneWrites = microtime( true );
*/
class DBUnexpectedError extends DBError {
}
+
+/**
+ * @ingroup Database
+ */
+class DBReadOnlyError extends DBError {
+}
$this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
$ret = $ret || $lb->hasMasterChanges();
} );
+
+ return $ret;
+ }
+
+ /**
+ * Detemine if any lagged slave connection was used
+ * @since 1.27
+ * @return bool
+ */
+ public function laggedSlaveUsed() {
+ $ret = false;
+ $this->forEachLB( function ( LoadBalancer $lb ) use ( &$ret ) {
+ $ret = $ret || $lb->laggedSlaveUsed();
+ } );
+
return $ret;
}
$trxProf->recordConnection( $host, $dbname, $masterOnly );
}
+ # Make master connections read only if in lagged slave mode
+ if ( $masterOnly && $this->getServerCount() > 1 && $this->getLaggedSlaveMode() ) {
+ $conn->setLBInfo( 'readOnlyReason',
+ 'The database has been automatically locked ' .
+ 'while the slave database servers catch up to the master'
+ );
+ }
+
return $conn;
}
}
/**
+ * @note This method will trigger a DB connection if not yet done
* @return bool Whether the generic connection for reads is highly "lagged"
*/
public function getLaggedSlaveMode() {
return $this->mLaggedSlaveMode;
}
+ /**
+ * @note This method will never cause a new DB connection
+ * @return bool Whether any generic connection used for reads was highly "lagged"
+ * @since 1.27
+ */
+ public function laggedSlaveUsed() {
+ return $this->mLaggedSlaveMode;
+ }
+
/**
* Disables/enables lag checks
* @param null|bool $mode
* Get a slave database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlave( $cluster ) {
global $wgDefaultExternalStore;
* Get a master database connection for the specified cluster
*
* @param string $cluster Cluster name
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMaster( $cluster ) {
$wiki = isset( $this->params['wiki'] ) ? $this->params['wiki'] : false;
* Helper function for self::batchFetchBlobs for merging master/slave results
* @param array &$ret Current self::batchFetchBlobs return value
* @param array &$ids Map from blob_id to requested itemIDs
- * @param mixed $res DB result from DatabaseBase::select
+ * @param mixed $res DB result from Database::select
*/
private function mergeBatchResult( array &$ret, array &$ids, $res ) {
foreach ( $res as $row ) {
protected function initFromGlobals() {
global $wgLocalFileRepo, $wgForeignFileRepos, $wgFileBackends;
+ // Register explicitly defined backends
+ $this->register( $wgFileBackends, wfConfiguredReadOnlyReason() );
+
$autoBackends = array();
// Automatically create b/c backends for file repos...
$repos = array_merge( $wgForeignFileRepos, array( $wgLocalFileRepo ) );
);
}
- $backends = array_merge( $autoBackends, $wgFileBackends );
-
- // Apply $wgReadOnly to all backends if not already read-only
- foreach ( $backends as &$backend ) {
- $backend['readOnly'] = !empty( $backend['readOnly'] )
- ? $backend['readOnly']
- : wfConfiguredReadOnlyReason();
- }
-
- $this->register( $backends );
+ // Register implicitly defined backends
+ $this->register( $autoBackends, wfConfiguredReadOnlyReason() );
}
/**
* Register an array of file backend configurations
*
* @param array $configs
+ * @param string|null $readOnlyReason
* @throws FileBackendException
*/
- protected function register( array $configs ) {
+ protected function register( array $configs, $readOnlyReason = null ) {
foreach ( $configs as $config ) {
if ( !isset( $config['name'] ) ) {
throw new FileBackendException( "Cannot register a backend with no name." );
}
$class = $config['class'];
+ $config['readOnly'] = !empty( $config['readOnly'] )
+ ? $config['readOnly']
+ : $readOnlyReason;
+
unset( $config['class'] ); // backend won't need this
$this->backends[$name] = array(
'class' => $class,
* @since 1.20
*/
class DBFileJournal extends FileJournal {
- /** @var DatabaseBase */
+ /** @var IDatabase */
protected $dbw;
protected $wiki = false; // string; wiki DB name
/**
* Get a master connection to the logging DB
*
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getMasterDB() {
* Get (or reuse) a connection to a lock DB
*
* @param string $lockDb
- * @return DatabaseBase
+ * @return IDatabase
* @throws DBError
*/
protected function getConnection( $lockDb ) {
* Do additional initialization for new lock DB connection
*
* @param string $lockDb
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @throws DBError
*/
- protected function initConnection( $lockDb, DatabaseBase $db ) {
+ protected function initConnection( $lockDb, IDatabase $db ) {
}
/**
/**
* @param string $lockDb
- * @param DatabaseBase $db
+ * @param IDatabase $db
*/
- protected function initConnection( $lockDb, DatabaseBase $db ) {
+ protected function initConnection( $lockDb, IDatabase $db ) {
# Let this transaction see lock rows from other transactions
$db->query( "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;" );
}
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMasterDB() {
if ( !isset( $this->dbConn ) ) {
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlaveDB() {
return $this->getMasterDB();
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getMasterDB() {
return wfGetDB( DB_MASTER, array(), $this->wiki );
}
/**
- * @return DatabaseBase
+ * @return IDatabase
*/
function getSlaveDB() {
return wfGetDB( DB_SLAVE, array(), $this->wiki );
function ( $oldValue, &$ttl, array &$setOpts ) use ( $that, $title ) {
$dbr = $that->getSlaveDB(); // possibly remote DB
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
if ( $title instanceof Title ) {
$row = $dbr->selectRow(
// Purge cache of all pages using this file
$title = $this->getTitle();
if ( $title ) {
- $update = new HTMLCacheUpdate( $title, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
}
}
// Cache presence for 1 week and negatives for 1 day
$ttl = $this->fileExists ? 86400 * 7 : 86400;
- $opts = DatabaseBase::getCacheSetOptions( $this->repo->getSlaveDB() );
+ $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
}
function ( $oldValue, &$ttl, array &$setOpts ) use ( $prefix ) {
$dbr = wfGetDB( DB_SLAVE );
- $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ $setOpts += Database::getCacheSetOptions( $dbr );
$row = $dbr->selectRow(
'interwiki',
// Re-ping all masters with transactions. This throws DBError if some
// connection died while waiting on locks/slaves, triggering a rollback.
wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $fname ) {
- $lb->forEachOpenConnection( function( DatabaseBase $conn ) use ( $fname ) {
+ $lb->forEachOpenConnection( function( IDatabase $conn ) use ( $fname ) {
if ( $conn->writesOrCallbacksPending() ) {
$conn->query( "SELECT 1", $fname );
}
--- /dev/null
+<?php
+/**
+ * APC-backed function memoization
+ *
+ * This class provides memoization for pure functions. A function is pure
+ * if its result value depends on nothing other than its input parameters
+ * and if invoking it does not cause any side-effects.
+ *
+ * The first invocation of the memoized callable with a particular set of
+ * arguments will be delegated to the underlying callable. Repeat invocations
+ * with the same input parameters will be served from APC.
+ *
+ * @par Example:
+ * @code
+ * $memoizedStrrev = new MemoizedCallable( 'range' );
+ * $memoizedStrrev->invoke( 5, 8 ); // result: array( 5, 6, 7, 8 )
+ * $memoizedStrrev->invokeArgs( array( 5, 8 ) ); // same
+ * MemoizedCallable::call( 'range', array( 5, 8 ) ); // same
+ * @endcode
+ *
+ * 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
+ * @author Ori Livneh
+ * @since 1.27
+ */
+class MemoizedCallable {
+
+ /** @var callable */
+ private $callable;
+
+ /** @var string Unique name of callable; used for cache keys. */
+ private $callableName;
+
+ /**
+ * Constructor.
+ *
+ * @throws InvalidArgumentException if $callable is not a callable.
+ * @param callable $callable Function or method to memoize.
+ * @param int $ttl TTL in seconds. Defaults to 3600 (1hr). Capped at 86400 (24h).
+ */
+ public function __construct( $callable, $ttl = 3600 ) {
+ if ( !is_callable( $callable, false, $this->callableName ) ) {
+ throw new InvalidArgumentException(
+ 'Argument 1 passed to MemoizedCallable::__construct() must ' .
+ 'be an instance of callable; ' . gettype( $callable ) . ' given'
+ );
+ }
+
+ if ( $this->callableName === 'Closure::__invoke' ) {
+ // Differentiate anonymous functions from one another
+ $this->callableName .= uniqid();
+ }
+
+ $this->callable = $callable;
+ $this->ttl = min( max( $ttl, 1 ), 86400 );
+ }
+
+ /**
+ * Fetch the result of a previous invocation from APC.
+ *
+ * @param string $key
+ * @param bool &$success
+ */
+ protected function fetchResult( $key, &$success ) {
+ $success = false;
+ if ( function_exists( 'apc_fetch' ) ) {
+ return apc_fetch( $key, $success );
+ }
+ return false;
+ }
+
+ /**
+ * Store the result of an invocation in APC.
+ *
+ * @param string $key
+ * @param mixed $result
+ */
+ protected function storeResult( $key, $result ) {
+ if ( function_exists( 'apc_store' ) ) {
+ apc_store( $key, $result, $this->ttl );
+ }
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * @throws InvalidArgumentException If parameters list contains non-scalar items.
+ * @param array $args Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invokeArgs( Array $args = array() ) {
+ foreach ( $args as $arg ) {
+ if ( $arg !== null && !is_scalar( $arg ) ) {
+ throw new InvalidArgumentException(
+ 'MemoizedCallable::invoke() called with non-scalar ' .
+ 'argument'
+ );
+ }
+ }
+
+ $hash = md5( serialize( $args ) );
+ $key = __CLASS__ . ':' . $this->callableName . ':' . $hash;
+ $success = false;
+ $result = $this->fetchResult( $key, $success );
+ if ( !$success ) {
+ $result = call_user_func_array( $this->callable, $args );
+ $this->storeResult( $key, $result );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Invoke the memoized function or method.
+ *
+ * Like MemoizedCallable::invokeArgs(), but variadic.
+ *
+ * @param mixed ...$params Parameters for memoized function or method.
+ * @return mixed The memoized callable's return value.
+ */
+ public function invoke() {
+ return $this->invokeArgs( func_get_args() );
+ }
+
+ /**
+ * Shortcut method for creating a MemoizedCallable and invoking it
+ * with the specified arguments.
+ *
+ * @param callable $callable
+ * @param array $args
+ * @param int $ttl
+ */
+ public static function call( $callable, Array $args = array(), $ttl = 3600 ) {
+ $instance = new self( $callable, $ttl );
+ return $instance->invokeArgs( $args );
+ }
+}
protected $maxConnsPerHost = 50;
/** @var string|null proxy */
protected $proxy;
+ /** @var string */
+ protected $userAgent = 'wikimedia/multi-http-client v1.0';
/**
* @param array $options
}
if ( !isset( $req['headers']['user-agent'] ) ) {
- $req['headers']['user-agent'] = self::userAgent();
+ $req['headers']['user-agent'] = $this->userAgent;
}
$headers = array();
curl_multi_close( $this->multiHandle );
}
}
-
- /**
- * The default User-Agent for requests.
- * @return string
- */
- public static function userAgent() {
- return 'wikimedia/multi-http-client v1.0';
- }
}
* Values in the arguments collection which are Closure instances will be
* expanded by invoking them with no arguments before passing the
* resulting value on to the constructor/callable. This can be used to
- * pass DatabaseBase instances or other live objects to the
+ * pass IDatabase instances or other live objects to the
* constructor/callable. This behavior can be suppressed by adding
* closure_expansion => false to the specification.
*
* - d) T1 reads the row and calls set() due to a cache miss
* - e) Stale value is stuck in cache
*
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
+ *
* Example usage:
* @code
* $dbr = wfGetDB( DB_SLAVE );
- * $setOpts = DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts = Database::getCacheSetOptions( $dbr );
* // Fetch the row from the DB
* $row = $dbr->selectRow( ... );
* $key = wfMemcKey( 'building', $buildingId );
* can be set dynamically by altering $ttl in the callback (by reference).
* The $setOpts array can be altered and is given to set() when called;
* it is recommended to set the 'since' field to avoid race conditions.
+ * Setting 'lag' helps avoids keys getting stuck in long-term stale states.
*
* Usually, callbacks ignore the current value, but it can be used
* to maintain "most recent X" values that come from time or sequence
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return $dbr->selectRow( ... );
* },
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatConfig::newFromRow( $dbr->selectRow( ... ) );
* },
* // Determine new value from the DB
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* return CatState::newFromResults( $dbr->select( ... ) );
* },
* function ( $oldValue, &$ttl, array &$setOpts ) {
* $dbr = wfGetDB( DB_SLAVE );
* // Account for any snapshot/slave lag
- * $setOpts += DatabaseBase::getCacheSetOptions( $dbr );
+ * $setOpts += Database::getCacheSetOptions( $dbr );
*
* // Start off with the last cached list
* $list = $oldValue ?: array();
* @return Status
*/
public static function send( $to, $from, $subject, $body, $options = array() ) {
- global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams, $wgAllowHTMLEmail;
+ global $wgAllowHTMLEmail;
$contentType = 'text/plain; charset=UTF-8';
- $headers = array();
- if ( is_array( $options ) ) {
- $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
- $contentType = isset( $options['contentType'] ) ? $options['contentType'] : $contentType;
- $headers = isset( $options['headers'] ) ? $options['headers'] : $headers;
- } else {
+ if ( !is_array( $options ) ) {
// Old calling style
wfDeprecated( __METHOD__ . ' with $replyto as 5th parameter', '1.26' );
- $replyto = $options;
+ $options = array( 'replyTo' => $options );
if ( func_num_args() === 6 ) {
- $contentType = func_get_arg( 5 );
+ $options['contentType'] = func_get_arg( 5 );
}
}
- $mime = null;
if ( !is_array( $to ) ) {
$to = array( $to );
}
return Status::newFatal( 'user-mail-no-addy' );
}
+ // give a chance to UserMailerTransformContents subscribers who need to deal with each
+ // target differently to split up the address list
+ if ( count( $to ) > 1 ) {
+ $oldTo = $to;
+ Hooks::run( 'UserMailerSplitTo', array( &$to ) );
+ if ( $oldTo != $to ) {
+ $splitTo = array_diff( $oldTo, $to );
+ $to = array_diff( $oldTo, $splitTo ); // ignore new addresses added in the hook
+ // first send to non-split address list, then to split addresses one by one
+ $status = Status::newGood();
+ if ( $to ) {
+ $status->merge( UserMailer::sendInternal(
+ $to, $from, $subject, $body, $options ) );
+ }
+ foreach ( $splitTo as $newTo ) {
+ $status->merge( UserMailer::sendInternal(
+ array( $newTo ), $from, $subject, $body, $options ) );
+ }
+ return $status;
+ }
+ }
+
+ return UserMailer::sendInternal( $to, $from, $subject, $body, $options );
+ }
+
+ /**
+ * Helper function fo UserMailer::send() which does the actual sending. It expects a $to
+ * list which the UserMailerSplitTo hook would not split further.
+ * @param MailAddress[] $to Array of recipients' email addresses
+ * @param MailAddress $from Sender's email
+ * @param string $subject Email's subject.
+ * @param string $body Email's text or Array of two strings to be the text and html bodies
+ * @param array $options:
+ * 'replyTo' MailAddress
+ * 'contentType' string default 'text/plain; charset=UTF-8'
+ * 'headers' array Extra headers to set
+ *
+ * @throws MWException
+ * @throws Exception
+ * @return Status
+ */
+ protected static function sendInternal(
+ array $to,
+ MailAddress $from,
+ $subject,
+ $body,
+ $options = array()
+ ) {
+ global $wgSMTP, $wgEnotifMaxRecips, $wgAdditionalMailParams;
+ $mime = null;
+
+ $replyto = isset( $options['replyTo'] ) ? $options['replyTo'] : null;
+ $contentType = isset( $options['contentType'] ) ?
+ $options['contentType'] : 'text/plain; charset=UTF-8';
+ $headers = isset( $options['headers'] ) ? $options['headers'] : array();
+
+ // Allow transformation of content, such as encrypting/signing
+ $error = false;
+ if ( !Hooks::run( 'UserMailerTransformContent', array( $to, $from, &$body, &$error ) ) ) {
+ if ( $error ) {
+ return Status::newFatal( 'php-mail-error', $error );
+ } else {
+ return Status::newFatal( 'php-mail-error-unknown' );
+ }
+ }
+
/**
* Forge email headers
* -------------------
$headers['Content-transfer-encoding'] = '8bit';
}
+ // allow transformation of MIME-encoded message
+ if ( !Hooks::run( 'UserMailerTransformMessage',
+ array( $to, $from, &$subject, &$headers, &$body, &$error ) )
+ ) {
+ if ( $error ) {
+ return Status::newFatal( 'php-mail-error', $error );
+ } else {
+ return Status::newFatal( 'php-mail-error-unknown' );
+ }
+ }
+
$ret = Hooks::run( 'AlternateUserMailer', array( $headers, $to, $from, $subject, $body ) );
if ( $ret === false ) {
// the hook implementation will return false to skip regular mail sending
* Get a connection to the specified database
*
* @param int $serverIndex
- * @return DatabaseBase
+ * @return IDatabase
* @throws MWException
*/
protected function getDB( $serverIndex ) {
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @param string $exptime
* @return bool
*/
}
/**
- * @param DatabaseBase $db
+ * @param IDatabase $db
* @return string
*/
protected function getMaxDateTime( $db ) {
$this->loadFile();
if ( $this->mFile->exists() ) {
wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() . "\n" );
- $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ) );
$this->mFile->upgradeRow();
$this->mFile->purgeCache( array( 'forThumbRefresh' => true ) );
} else {
// Images
if ( $title->getNamespace() == NS_FILE ) {
- $update = new HTMLCacheUpdate( $title, 'imagelinks' );
- $update->doUpdate();
+ DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
}
// User talk pages
/**
* Get the Database object in use
*
- * @return DatabaseBase
+ * @return IDatabase
*/
public function getDatabase() {
return $this->mDb;
$this->missingLocalFileRefs[] = $file;
}
}
- return CSSMin::remap(
- $style, $localDir, $remoteDir, true
- );
+ return MemoizedCallable::call( 'CSSMin::remap',
+ array( $style, $localDir, $remoteDir, true ) );
}
/**
class BatchRowIterator implements RecursiveIterator {
/**
- * @var DatabaseBase $db The database to read from
+ * @var IDatabase $db The database to read from
*/
protected $db;
/**
* @var array $fetchColumns List of column names to select from the
- * table suitable for use with DatabaseBase::select()
+ * table suitable for use with IDatabase::select()
*/
protected $fetchColumns;
/**
* @param array $condition Query conditions suitable for use with
- * DatabaseBase::select
+ * IDatabase::select
*/
public function addConditions( array $conditions ) {
$this->conditions = array_merge( $this->conditions, $conditions );
/**
* @param array $condition Query join conditions suitable for use
- * with DatabaseBase::select
+ * with IDatabase::select
*/
public function addJoinConditions( array $conditions ) {
$this->joinConditions = array_merge( $this->joinConditions, $conditions );
/**
* @param array $columns List of column names to select from the
- * table suitable for use with DatabaseBase::select()
+ * table suitable for use with IDatabase::select()
*/
public function setFetchColumns( array $columns ) {
// If it's not the all column selector merge in the primary keys we need
*/
class BatchRowWriter {
/**
- * @var DatabaseBase $db The database to write to
+ * @var IDatabase $db The database to write to
*/
protected $db;
--- /dev/null
+<?php
+/**
+ * A MemoizedCallable subclass that stores function return values
+ * in an instance property rather than APC.
+ */
+class ArrayBackedMemoizedCallable extends MemoizedCallable {
+ public $cache = array();
+
+ protected function fetchResult( $key, &$success ) {
+ if ( array_key_exists( $key, $this->cache ) ) {
+ $success = true;
+ return $this->cache[$key];
+ }
+ $success = false;
+ return false;
+ }
+
+ protected function storeResult( $key, $result ) {
+ $this->cache[$key] = $result;
+ }
+}
+
+
+/**
+ * PHP Unit tests for MemoizedCallable class.
+ * @covers MemoizedCallable
+ */
+class MemoizedCallableTest extends PHPUnit_Framework_TestCase {
+
+ /**
+ * The memoized callable should relate inputs to outputs in the same
+ * way as the original underlying callable.
+ */
+ public function testReturnValuePassedThrough() {
+ $mock = $this->getMock( 'stdClass', array( 'reverse' ) );
+ $mock->expects( $this->any() )
+ ->method( 'reverse' )
+ ->will( $this->returnCallback( 'strrev' ) );
+
+ $memoized = new MemoizedCallable( array( $mock, 'reverse' ) );
+ $this->assertEquals( 'flow', $memoized->invoke( 'wolf' ) );
+ }
+
+ /**
+ * Consecutive calls to the memoized callable with the same arguments
+ * should result in just one invocation of the underlying callable.
+ *
+ * @requires function apc_store
+ */
+ public function testCallableMemoized() {
+ $observer = $this->getMock( 'stdClass', array( 'computeSomething' ) );
+ $observer->expects( $this->once() )
+ ->method( 'computeSomething' )
+ ->will( $this->returnValue( 'ok' ) );
+
+ $memoized = new ArrayBackedMemoizedCallable( array( $observer, 'computeSomething' ) );
+
+ // First invocation -- delegates to $observer->computeSomething()
+ $this->assertEquals( 'ok', $memoized->invoke() );
+
+ // Second invocation -- returns memoized result
+ $this->assertEquals( 'ok', $memoized->invoke() );
+ }
+
+ /**
+ * @covers MemoizedCallable::invoke
+ */
+ public function testInvokeVariadic() {
+ $memoized = new MemoizedCallable( 'sprintf' );
+ $this->assertEquals(
+ $memoized->invokeArgs( array( 'this is %s', 'correct' ) ),
+ $memoized->invoke( 'this is %s', 'correct' )
+ );
+ }
+
+ /**
+ * @covers MemoizedCallable::call
+ */
+ public function testShortcutMethod() {
+ $this->assertEquals(
+ 'this is correct',
+ MemoizedCallable::call( 'sprintf', array( 'this is %s', 'correct' ) )
+ );
+ }
+
+ /**
+ * Outlier TTL values should be coerced to range 1 - 86400.
+ */
+ public function testTTLMaxMin() {
+ $memoized = new MemoizedCallable( 'abs', 100000 );
+ $this->assertEquals( 86400, $this->readAttribute( $memoized, 'ttl' ) );
+
+ $memoized = new MemoizedCallable( 'abs', -10 );
+ $this->assertEquals( 1, $this->readAttribute( $memoized, 'ttl' ) );
+ }
+
+ /**
+ * Closure names should be distinct.
+ */
+ public function testMemoizedClosure() {
+ $a = new MemoizedCallable( function () {
+ return 'a';
+ } );
+
+ $b = new MemoizedCallable( function () {
+ return 'b';
+ } );
+
+ $this->assertEquals( $a->invokeArgs(), 'a' );
+ $this->assertEquals( $b->invokeArgs(), 'b' );
+
+ $this->assertNotEquals(
+ $this->readAttribute( $a, 'callableName' ),
+ $this->readAttribute( $b, 'callableName' )
+ );
+ }
+
+ /**
+ * @expectedExceptionMessage non-scalar argument
+ * @expectedException InvalidArgumentException
+ */
+ public function testNonScalarArguments() {
+ $memoized = new MemoizedCallable( 'gettype' );
+ $memoized->invoke( new stdClass() );
+ }
+
+ /**
+ * @expectedExceptionMessage must be an instance of callable
+ * @expectedException InvalidArgumentException
+ */
+ public function testNotCallable() {
+ $memoized = new MemoizedCallable( 14 );
+ }
+}