the mw.Api instance seems to be for the local wiki.
* After a client performs an action which alters a database that has replica databases,
MediaWiki will wait for the replica databases to synchronize with the master database
- while it renders the HTML output. However, if the output is a redirect, it will instead
- alter the redirect URL to include a ?cpPosTime parameter that triggers the database
- synchronization when the URL is followed by the client.
+ while it renders the HTML output. However, if the output is a redirect to another wiki
+ on the wiki farm with a different domain, MediaWiki will instead alter the redirect
+ URL to include a ?cpPosTime parameter that triggers the database synchronization when
+ the URL is followed by the client. The same-domain case uses a new cpPosTime cookie.
=== External library changes in 1.28 ===
'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php',
'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php',
'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php',
+ 'ILoadBalancer' => __DIR__ . '/includes/db/loadbalancer/ILoadBalancer.php',
'IP' => __DIR__ . '/includes/utils/IP.php',
'IPSet' => __DIR__ . '/includes/compat/IPSetCompat.php',
'IPTC' => __DIR__ . '/includes/media/IPTC.php',
'ListredirectsPage' => __DIR__ . '/includes/specials/SpecialListredirects.php',
'LoadBalancer' => __DIR__ . '/includes/db/loadbalancer/LoadBalancer.php',
'LoadBalancerSingle' => __DIR__ . '/includes/db/loadbalancer/LBFactorySingle.php',
- 'LoadMonitor' => __DIR__ . '/includes/db/loadbalancer/LoadMonitor.php',
- 'LoadMonitorMySQL' => __DIR__ . '/includes/db/loadbalancer/LoadMonitorMySQL.php',
- 'LoadMonitorNull' => __DIR__ . '/includes/db/loadbalancer/LoadMonitor.php',
+ 'LoadMonitor' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitor.php',
+ 'LoadMonitorMySQL' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorMySQL.php',
+ 'LoadMonitorNull' => __DIR__ . '/includes/libs/rdbms/loadmonitor/LoadMonitorNull.php',
'LocalFile' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
'LocalFileDeleteBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
'LocalFileLockError' => __DIR__ . '/includes/filerepo/file/LocalFile.php',
'MemcLockManager' => __DIR__ . '/includes/filebackend/lockmanager/MemcLockManager.php',
'MemcachedBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedBagOStuff.php',
'MemcachedClient' => __DIR__ . '/includes/libs/objectcache/MemcachedClient.php',
- 'MemcachedPeclBagOStuff' => __DIR__ . '/includes/objectcache/MemcachedPeclBagOStuff.php',
+ 'MemcachedPeclBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPeclBagOStuff.php',
'MemcachedPhpBagOStuff' => __DIR__ . '/includes/libs/objectcache/MemcachedPhpBagOStuff.php',
'MemoizedCallable' => __DIR__ . '/includes/libs/MemoizedCallable.php',
'MemoryFileBackend' => __DIR__ . '/includes/filebackend/MemoryFileBackend.php',
wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
// Decide when clients block on ChronologyProtector DB position writes
- if (
+ $urlDomainDistance = (
$request->wasPosted() &&
$output->getRedirect() &&
- $lbFactory->hasOrMadeRecentMasterChanges( INF ) &&
- self::isWikiClusterURL( $output->getRedirect(), $context )
- ) {
+ $lbFactory->hasOrMadeRecentMasterChanges( INF )
+ ) ? self::getUrlDomainDistance( $output->getRedirect(), $context ) : false;
+
+ if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) {
// OutputPage::output() will be fast; $postCommitWork will not be useful for
// masking the latency of syncing DB positions accross all datacenters synchronously.
// Instead, make use of the RTT time of the client follow redirects.
$flags = $lbFactory::SHUTDOWN_CHRONPROT_ASYNC;
+ $cpPosTime = microtime( true );
// Client's next request should see 1+ positions with this DBMasterPos::asOf() time
- $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
- $output->getRedirect(),
- microtime( true )
- );
- $output->redirect( $safeUrl );
+ if ( $urlDomainDistance === 'local' ) {
+ // Client will stay on this domain, so set an unobtrusive cookie
+ $expires = time() + ChronologyProtector::POSITION_TTL;
+ $options = [ 'prefix' => '' ];
+ $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+ } else {
+ // Cookies may not work across wiki domains, so use a URL parameter
+ $safeUrl = $lbFactory->appendPreShutdownTimeAsQuery(
+ $output->getRedirect(),
+ $cpPosTime
+ );
+ $output->redirect( $safeUrl );
+ }
} else {
// OutputPage::output() is fairly slow; run it in $postCommitWork to mask
// the latency of syncing DB positions accross all datacenters synchronously
$flags = $lbFactory::SHUTDOWN_CHRONPROT_SYNC;
+ if ( $lbFactory->hasOrMadeRecentMasterChanges( INF ) ) {
+ $cpPosTime = microtime( true );
+ // Set a cookie in case the DB position store cannot sync accross datacenters.
+ // This will at least cover the common case of the user staying on the domain.
+ $expires = time() + ChronologyProtector::POSITION_TTL;
+ $options = [ 'prefix' => '' ];
+ $request->response()->setCookie( 'cpPosTime', $cpPosTime, $expires, $options );
+ }
}
// Record ChronologyProtector positions for DBs affected in this request at this point
$lbFactory->shutdown( $flags, $postCommitWork );
/**
* @param string $url
* @param IContextSource $context
- * @return bool Whether $url is to something on this wiki farm
+ * @return string|bool Either "local" or "remote" if in the farm, false otherwise
*/
- private function isWikiClusterURL( $url, IContextSource $context ) {
+ private function getUrlDomainDistance( $url, IContextSource $context ) {
static $relevantKeys = [ 'host' => true, 'port' => true ];
$infoCandidate = wfParseUrl( $url );
$context->getConfig()->get( 'LocalVirtualHosts' )
);
- foreach ( $clusterHosts as $clusterHost ) {
+ foreach ( $clusterHosts as $i => $clusterHost ) {
$parseUrl = wfParseUrl( $clusterHost );
if ( !$parseUrl ) {
continue;
}
$infoHost = array_intersect_key( $parseUrl, $relevantKeys );
if ( $infoCandidate === $infoHost ) {
- return true;
+ return ( $i === 0 ) ? 'local' : 'remote';
}
}
$title = $this->getTitle();
// Never send an RC notification email about categorization changes
- if ( $this->mAttribs['rc_type'] != RC_CATEGORIZE ) {
- if ( Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] ) ) {
- # @todo FIXME: This would be better as an extension hook
+ if (
+ $this->mAttribs['rc_type'] != RC_CATEGORIZE &&
+ Hooks::run( 'AbortEmailNotification', [ $editor, $title, $this ] )
+ ) {
+ // @FIXME: This would be better as an extension hook
+ // Send emails or email jobs once this row is safely committed
+ $dbw->onTransactionIdle( function () use ( $editor, $title ) {
$enotif = new EmailNotification();
$enotif->notifyOnPageChange(
$editor,
$this->mAttribs['rc_last_oldid'],
$this->mExtra['pageStatus']
);
- }
+ } );
}
}
* @file
* @ingroup Database
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use MediaWiki\Logger\LoggerFactory;
/**
* Class for ensuring a consistent ordering of events as seen by the user, despite replication.
* Kind of like Hawking's [[Chronology Protection Agency]].
*/
-class ChronologyProtector {
+class ChronologyProtector implements LoggerAwareInterface{
/** @var BagOStuff */
protected $store;
+ /** @var LoggerInterface */
+ protected $logger;
/** @var string Storage key name */
protected $key;
$this->clientId = md5( $client['ip'] . "\n" . $client['agent'] );
$this->key = $store->makeGlobalKey( __CLASS__, $this->clientId );
$this->waitForPosTime = $posTime;
+ $this->logger = LoggerFactory::getInstance( 'DBReplication' );
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
}
/**
$masterName = $lb->getServerName( $lb->getWriterIndex() );
if ( !empty( $this->startupPositions[$masterName] ) ) {
$pos = $this->startupPositions[$masterName];
- wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
+ $this->logger->info( __METHOD__ . ": LB for '$masterName' set to pos $pos\n" );
$lb->waitFor( $pos );
}
}
$masterName = $lb->getServerName( $lb->getWriterIndex() );
if ( $lb->getServerCount() > 1 ) {
$pos = $lb->getMasterPos();
- wfDebugLog( 'replication', __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
+ $this->logger->info( __METHOD__ . ": LB for '$masterName' has pos $pos\n" );
$this->shutdownPositions[$masterName] = $pos;
} else {
- wfDebugLog( 'replication', __METHOD__ . ": DB '$masterName' touched\n" );
+ $this->logger->info( __METHOD__ . ": DB '$masterName' touched\n" );
}
$this->shutdownTouchDBs[$masterName] = 1;
}
return []; // nothing to save
}
- wfDebugLog( 'replication',
- __METHOD__ . ": saving master pos for " .
+ $this->logger->info( __METHOD__ . ": saving master pos for " .
implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
);
if ( !$ok ) {
$bouncedPositions = $this->shutdownPositions;
// Raced out too many times or stash is down
- wfDebugLog( 'replication',
- __METHOD__ . ": failed to save master pos for " .
+ $this->logger->warning( __METHOD__ . ": failed to save master pos for " .
implode( ', ', array_keys( $this->shutdownPositions ) ) . "\n"
);
} elseif ( $mode === 'sync' &&
$store->getQoS( $store::ATTR_SYNCWRITES ) < $store::QOS_SYNCWRITES_BE
) {
// Positions may not be in all datacenters, force LBFactory to play it safe
- wfDebugLog( 'replication',
- __METHOD__ . ": store does not report ability to sync replicas. " );
+ $this->logger->info( __METHOD__ . ": store may not support synchronous writes." );
$bouncedPositions = $this->shutdownPositions;
} else {
$bouncedPositions = [];
if ( $this->wait ) {
// If there is an expectation to see master positions with a certain min
// timestamp, then block until they appear, or until a timeout is reached.
- if ( $this->waitForPosTime ) {
+ if ( $this->waitForPosTime > 0.0 ) {
$data = null;
$loop = new WaitConditionLoop(
function () use ( &$data ) {
}
$this->startupPositions = $data ? $data['positions'] : [];
- wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (read)\n" );
+ $this->logger->info( __METHOD__ . ": key is {$this->key} (read)\n" );
} else {
$this->startupPositions = [];
- wfDebugLog( 'replication', __METHOD__ . ": key is {$this->key} (unread)\n" );
+ $this->logger->info( __METHOD__ . ": key is {$this->key} (unread)\n" );
}
}
if ( $wgSharedDB && in_array( $tbl, $wgSharedTables, true ) ) {
// Shared tables don't work properly when cloning due to
// how prefixes are handled (bug 65654)
- throw new MWException( "Cannot clone shared table $tbl." );
+ throw new RuntimeException( "Cannot clone shared table $tbl." );
}
# Clean up from previous aborted run. So that table escaping
# works correctly across DB engines, we need to change the pre-
) {
if ( $oldTableName === $newTableName ) {
// Last ditch check to avoid data loss
- throw new MWException( "Not dropping new table, as '$newTableName'"
+ throw new LogicException( "Not dropping new table, as '$newTableName'"
. " is name of both the old and the new table." );
}
$this->db->dropTable( $tbl, __METHOD__ );
*/
public static function changePrefix( $prefix ) {
global $wgDBprefix;
- wfGetLBFactory()->forEachLB( function( $lb ) use ( $prefix ) {
- $lb->forEachOpenConnection( function ( $db ) use ( $prefix ) {
+ wfGetLBFactory()->forEachLB( function( LoadBalancer $lb ) use ( $prefix ) {
+ $lb->setDomainPrefix( $prefix );
+ $lb->forEachOpenConnection( function ( DatabaseBase $db ) use ( $prefix ) {
$db->tablePrefix( $prefix );
} );
} );
/** @var array|null */
private $params;
+ const FLD_INDEX = 0;
+ const FLD_GROUP = 1;
+ const FLD_WIKI = 2;
+
/**
* @param LoadBalancer $lb
- * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID) array
+ * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID)
*/
public function __construct( LoadBalancer $lb, $conn ) {
$this->lb = $lb;
if ( $conn instanceof DatabaseBase ) {
$this->conn = $conn;
- } else {
+ } elseif ( count( $conn ) >= 3 && $conn[self::FLD_WIKI] !== false ) {
$this->params = $conn;
+ } else {
+ throw new InvalidArgumentException( "Missing lazy connection arguments." );
}
}
}
public function getWikiID() {
+ if ( $this->conn === null ) {
+ // Avoid triggering a connection
+ return $this->params[self::FLD_WIKI];
+ }
+
return $this->__call( __FUNCTION__, func_get_args() );
}
<?php
-
/**
* @defgroup Database Database
*
* @file
* @ingroup Database
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
/**
* Database abstraction object
* @ingroup Database
*/
-abstract class DatabaseBase implements IDatabase {
+abstract class DatabaseBase implements IDatabase, LoggerAwareInterface {
/** Number of times to re-try an operation in case of deadlock */
const DEADLOCK_TRIES = 4;
/** Minimum time to wait before retry, in microseconds */
/** @var BagOStuff APC cache */
protected $srvCache;
+ /** @var LoggerInterface */
+ protected $connLogger;
+ /** @var LoggerInterface */
+ protected $queryLogger;
/** @var resource Database connection */
protected $mConn = null;
/** @var TransactionProfiler */
protected $trxProfiler;
+ /**
+ * Constructor.
+ *
+ * FIXME: It is possible to construct a Database object with no associated
+ * connection object, by specifying no parameters to __construct(). This
+ * feature is deprecated and should be removed.
+ *
+ * DatabaseBase subclasses should not be constructed directly in external
+ * code. DatabaseBase::factory() should be used instead.
+ *
+ * @param array $params Parameters passed from DatabaseBase::factory()
+ */
+ function __construct( array $params ) {
+ global $wgDBprefix, $wgDBmwschema;
+
+ $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
+
+ $server = $params['host'];
+ $user = $params['user'];
+ $password = $params['password'];
+ $dbName = $params['dbname'];
+ $flags = $params['flags'];
+ $tablePrefix = $params['tablePrefix'];
+ $schema = $params['schema'];
+ $foreign = $params['foreign'];
+
+ $this->cliMode = isset( $params['cliMode'] )
+ ? $params['cliMode']
+ : ( PHP_SAPI === 'cli' );
+
+ $this->mFlags = $flags;
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $this->cliMode ) {
+ $this->mFlags &= ~DBO_TRX;
+ } else {
+ $this->mFlags |= DBO_TRX;
+ }
+ }
+
+ $this->mSessionVars = $params['variables'];
+
+ /** Get the default table prefix*/
+ if ( $tablePrefix === 'get from global' ) {
+ $this->mTablePrefix = $wgDBprefix;
+ } else {
+ $this->mTablePrefix = $tablePrefix;
+ }
+
+ /** Get the database schema*/
+ if ( $schema === 'get from global' ) {
+ $this->mSchema = $wgDBmwschema;
+ } else {
+ $this->mSchema = $schema;
+ }
+
+ $this->mForeign = $foreign;
+
+ $this->profiler = isset( $params['profiler'] )
+ ? $params['profiler']
+ : Profiler::instance(); // @TODO: remove global state
+ $this->trxProfiler = isset( $params['trxProfiler'] )
+ ? $params['trxProfiler']
+ : new TransactionProfiler();
+ $this->connLogger = isset( $params['connLogger'] )
+ ? $params['connLogger']
+ : new \Psr\Log\NullLogger();
+ $this->queryLogger = isset( $params['queryLogger'] )
+ ? $params['queryLogger']
+ : new \Psr\Log\NullLogger();
+
+ if ( $user ) {
+ $this->open( $server, $user, $password, $dbName );
+ }
+ }
+
+ /**
+ * Given a DB type, construct the name of the appropriate child class of
+ * DatabaseBase. This is designed to replace all of the manual stuff like:
+ * $class = 'Database' . ucfirst( strtolower( $dbType ) );
+ * as well as validate against the canonical list of DB types we have
+ *
+ * This factory function is mostly useful for when you need to connect to a
+ * database other than the MediaWiki default (such as for external auth,
+ * an extension, et cetera). Do not use this to connect to the MediaWiki
+ * database. Example uses in core:
+ * @see LoadBalancer::reallyOpenConnection()
+ * @see ForeignDBRepo::getMasterDB()
+ * @see WebInstallerDBConnect::execute()
+ *
+ * @since 1.18
+ *
+ * @param string $dbType A possible DB type
+ * @param array $p An array of options to pass to the constructor.
+ * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
+ * @throws MWException If the database driver or extension cannot be found
+ * @return DatabaseBase|null DatabaseBase subclass or null
+ */
+ final public static function factory( $dbType, $p = [] ) {
+ global $wgCommandLineMode;
+
+ $canonicalDBTypes = [
+ 'mysql' => [ 'mysqli', 'mysql' ],
+ 'postgres' => [],
+ 'sqlite' => [],
+ 'oracle' => [],
+ 'mssql' => [],
+ ];
+
+ $driver = false;
+ $dbType = strtolower( $dbType );
+ if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
+ $possibleDrivers = $canonicalDBTypes[$dbType];
+ if ( !empty( $p['driver'] ) ) {
+ if ( in_array( $p['driver'], $possibleDrivers ) ) {
+ $driver = $p['driver'];
+ } else {
+ throw new InvalidArgumentException( __METHOD__ .
+ " type '$dbType' does not support driver '{$p['driver']}'" );
+ }
+ } else {
+ foreach ( $possibleDrivers as $posDriver ) {
+ if ( extension_loaded( $posDriver ) ) {
+ $driver = $posDriver;
+ break;
+ }
+ }
+ }
+ } else {
+ $driver = $dbType;
+ }
+ if ( $driver === false ) {
+ throw new MWException( __METHOD__ .
+ " no viable database extension found for type '$dbType'" );
+ }
+
+ // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
+ // and everything else doesn't use a schema (e.g. null)
+ // Although postgres and oracle support schemas, we don't use them (yet)
+ // to maintain backwards compatibility
+ $defaultSchemas = [
+ 'mssql' => 'get from global',
+ ];
+
+ $class = 'Database' . ucfirst( $driver );
+ if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
+ // Resolve some defaults for b/c
+ $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
+ $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
+ $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
+ $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
+ $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
+ $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
+ $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
+ if ( !isset( $p['schema'] ) ) {
+ $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
+ }
+ $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
+ $p['cliMode'] = $wgCommandLineMode;
+
+ $conn = new $class( $p );
+ if ( isset( $p['connLogger'] ) ) {
+ $conn->connLogger = $p['connLogger'];
+ }
+ if ( isset( $p['queryLogger'] ) ) {
+ $conn->queryLogger = $p['queryLogger'];
+ }
+ } else {
+ $conn = null;
+ }
+
+ return $conn;
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->quertLogger = $logger;
+ }
+
public function getServerInfo() {
return $this->getServerVersion();
}
*/
abstract function strencode( $s );
- /**
- * Constructor.
- *
- * FIXME: It is possible to construct a Database object with no associated
- * connection object, by specifying no parameters to __construct(). This
- * feature is deprecated and should be removed.
- *
- * DatabaseBase subclasses should not be constructed directly in external
- * code. DatabaseBase::factory() should be used instead.
- *
- * @param array $params Parameters passed from DatabaseBase::factory()
- */
- function __construct( array $params ) {
- global $wgDBprefix, $wgDBmwschema;
-
- $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
-
- $server = $params['host'];
- $user = $params['user'];
- $password = $params['password'];
- $dbName = $params['dbname'];
- $flags = $params['flags'];
- $tablePrefix = $params['tablePrefix'];
- $schema = $params['schema'];
- $foreign = $params['foreign'];
-
- $this->cliMode = isset( $params['cliMode'] )
- ? $params['cliMode']
- : ( PHP_SAPI === 'cli' );
-
- $this->mFlags = $flags;
- if ( $this->mFlags & DBO_DEFAULT ) {
- if ( $this->cliMode ) {
- $this->mFlags &= ~DBO_TRX;
- } else {
- $this->mFlags |= DBO_TRX;
- }
- }
-
- $this->mSessionVars = $params['variables'];
-
- /** Get the default table prefix*/
- if ( $tablePrefix === 'get from global' ) {
- $this->mTablePrefix = $wgDBprefix;
- } else {
- $this->mTablePrefix = $tablePrefix;
- }
-
- /** Get the database schema*/
- if ( $schema === 'get from global' ) {
- $this->mSchema = $wgDBmwschema;
- } else {
- $this->mSchema = $schema;
- }
-
- $this->mForeign = $foreign;
-
- $this->profiler = isset( $params['profiler'] )
- ? $params['profiler']
- : Profiler::instance(); // @TODO: remove global state
- $this->trxProfiler = isset( $params['trxProfiler'] )
- ? $params['trxProfiler']
- : new TransactionProfiler();
-
- if ( $user ) {
- $this->open( $server, $user, $password, $dbName );
- }
-
- }
-
/**
* Called by serialize. Throw an exception when DB connection is serialized.
* This causes problems on some database engines because the connection is
* not restored on unserialize.
*/
public function __sleep() {
- throw new MWException( 'Database serialization may cause problems, since ' .
+ throw new RuntimeException( 'Database serialization may cause problems, since ' .
'the connection is not restored on wakeup.' );
}
- /**
- * Given a DB type, construct the name of the appropriate child class of
- * DatabaseBase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $dbType ) );
- * as well as validate against the canonical list of DB types we have
- *
- * This factory function is mostly useful for when you need to connect to a
- * database other than the MediaWiki default (such as for external auth,
- * an extension, et cetera). Do not use this to connect to the MediaWiki
- * database. Example uses in core:
- * @see LoadBalancer::reallyOpenConnection()
- * @see ForeignDBRepo::getMasterDB()
- * @see WebInstallerDBConnect::execute()
- *
- * @since 1.18
- *
- * @param string $dbType A possible DB type
- * @param array $p An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
- * @throws MWException If the database driver or extension cannot be found
- * @return DatabaseBase|null DatabaseBase subclass or null
- */
- final public static function factory( $dbType, $p = [] ) {
- global $wgCommandLineMode;
-
- $canonicalDBTypes = [
- 'mysql' => [ 'mysqli', 'mysql' ],
- 'postgres' => [],
- 'sqlite' => [],
- 'oracle' => [],
- 'mssql' => [],
- ];
-
- $driver = false;
- $dbType = strtolower( $dbType );
- if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
- $possibleDrivers = $canonicalDBTypes[$dbType];
- if ( !empty( $p['driver'] ) ) {
- if ( in_array( $p['driver'], $possibleDrivers ) ) {
- $driver = $p['driver'];
- } else {
- throw new MWException( __METHOD__ .
- " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
- }
- } else {
- foreach ( $possibleDrivers as $posDriver ) {
- if ( extension_loaded( $posDriver ) ) {
- $driver = $posDriver;
- break;
- }
- }
- }
- } else {
- $driver = $dbType;
- }
- if ( $driver === false ) {
- throw new MWException( __METHOD__ .
- " no viable database extension found for type '$dbType'" );
- }
-
- // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
- // and everything else doesn't use a schema (e.g. null)
- // Although postgres and oracle support schemas, we don't use them (yet)
- // to maintain backwards compatibility
- $defaultSchemas = [
- 'mssql' => 'get from global',
- ];
-
- $class = 'Database' . ucfirst( $driver );
- if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
- // Resolve some defaults for b/c
- $p['host'] = isset( $p['host'] ) ? $p['host'] : false;
- $p['user'] = isset( $p['user'] ) ? $p['user'] : false;
- $p['password'] = isset( $p['password'] ) ? $p['password'] : false;
- $p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
- $p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
- $p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
- $p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
- if ( !isset( $p['schema'] ) ) {
- $p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
- }
- $p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
- $p['cliMode'] = $wgCommandLineMode;
-
- return new $class( $p );
- } else {
- return null;
- }
- }
-
protected function installErrorHandler() {
$this->mPHPError = false;
$this->htmlErrors = ini_set( 'html_errors', '0' );
$closed = $this->closeConnection();
$this->mConn = false;
} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
- throw new MWException( "Transaction callbacks still pending." );
+ throw new RuntimeException( "Transaction callbacks still pending." );
} else {
$closed = true;
}
}
if ( $this->debug() ) {
- wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
+ $this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
}
# Avoid fatals if close() was called
$lastErrno = $this->lastErrno();
# Update state tracking to reflect transaction loss due to disconnection
$this->handleTransactionLoss();
- wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->reconnect() ) {
- wfDebug( "Reconnected\n" );
$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+ $this->connLogger->warning( $msg );
+ $this->queryLogger->warning( "$msg:\n" . wfBacktrace( true ) );
if ( !$recoverable ) {
# Callers may catch the exception and continue to use the DB
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
}
} else {
- wfDebug( "Failed\n" );
+ $msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
+ $this->connLogger->error( $msg );
}
}
public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
if ( $this->ignoreErrors() || $tempIgnore ) {
- wfDebug( "SQL ERROR (ignored): $error\n" );
+ $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
wfLogDBError(
'fname' => $fname,
] )
);
- wfDebug( "SQL ERROR: " . $error . "\n" );
+ $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
throw new DBQueryError( $this, $error, $errno, $sql, $fname );
}
}
unset( $value[$nullKey] );
}
if ( count( $value ) == 0 && !$includeNull ) {
- throw new MWException( __METHOD__ . ": empty input for field $field" );
+ throw new InvalidArgumentException( __METHOD__ . ": empty input for field $field" );
} elseif ( count( $value ) == 0 ) {
// only check if $field is null
$list .= "$field IS NULL";
public function duplicateTableStructure( $oldName, $newName, $temporary = false,
$fname = __METHOD__
) {
- throw new MWException(
+ throw new RuntimeException(
'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
}
function listTables( $prefix = null, $fname = __METHOD__ ) {
- throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
+ throw new RuntimeException( 'DatabaseBase::listTables is not implemented in descendant class' );
}
/**
* @since 1.22
*/
public function listViews( $prefix = null, $fname = __METHOD__ ) {
- throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
+ throw new RuntimeException( 'DatabaseBase::listViews is not implemented in descendant class' );
}
/**
* @since 1.22
*/
public function isView( $name ) {
- throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
+ throw new RuntimeException( 'DatabaseBase::isView is not implemented in descendant class' );
}
public function timestamp( $ts = 0 ) {
MediaWiki\restoreWarnings();
if ( false === $fp ) {
- throw new MWException( "Could not open \"{$filename}\".\n" );
+ throw new RuntimeException( "Could not open \"{$filename}\".\n" );
}
if ( !$fname ) {
*/
private function escapeIdentifier( $identifier ) {
if ( strlen( $identifier ) == 0 ) {
- throw new MWException( "An identifier must not be empty" );
+ throw new InvalidArgumentException( "An identifier must not be empty" );
}
if ( strlen( $identifier ) > 128 ) {
- throw new MWException( "The identifier '$identifier' is too long (max. 128)" );
+ throw new InvalidArgumentException( "The identifier '$identifier' is too long (max. 128)" );
}
if ( ( strpos( $identifier, '[' ) !== false )
|| ( strpos( $identifier, ']' ) !== false )
) {
// It may be allowed if you quoted with double quotation marks, but
// that would break if QUOTED_IDENTIFIER is OFF
- throw new MWException( "Square brackets are not allowed in '$identifier'" );
+ throw new InvalidArgumentException( "Square brackets are not allowed in '$identifier'" );
}
return "[$identifier]";
protected function doQuery( $sql ) {
wfDebug( "SQL: [$sql]\n" );
if ( !StringUtils::isUtf8( $sql ) ) {
- throw new MWException( "SQL encoding is invalid\n$sql" );
+ throw new InvalidArgumentException( "SQL encoding is invalid\n$sql" );
}
// handle some oracle specifics
$this->addQuotes( $oldName ) . " AND type='table'", $fname );
$obj = $this->fetchObject( $res );
if ( !$obj ) {
- throw new MWException( "Couldn't retrieve structure for table $oldName" );
+ throw new RuntimeException( "Couldn't retrieve structure for table $oldName" );
}
$sql = $obj->sql;
$sql = preg_replace(
--- /dev/null
+<?php
+/**
+ * Database load balancing interface
+ *
+ * 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
+ * @ingroup Database
+ * @author Aaron Schulz
+ */
+
+/**
+ * Interface for database load balancing object that manages IDatabase handles
+ *
+ * @todo: loosen up DB classes from MWException
+ * @since 1.28
+ * @ingroup Database
+ */
+interface ILoadBalancer {
+ /**
+ * @param array $params Array with keys:
+ * - servers : Required. Array of server info structures.
+ * - loadMonitor : Name of a class used to fetch server lag and load.
+ * - readOnlyReason : Reason the master DB is read-only if so [optional]
+ * - waitTimeout : Maximum time to wait for replicas for consistency [optional]
+ * - srvCache : BagOStuff object [optional]
+ * - wanCache : WANObjectCache object [optional]
+ * @throws MWException
+ */
+ public function __construct( array $params );
+
+ /**
+ * Get the index of the reader connection, which may be a replica DB
+ * This takes into account load ratios and lag times. It should
+ * always return a consistent index during a given invocation
+ *
+ * Side effect: opens connections to databases
+ * @param string|bool $group Query group, or false for the generic reader
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @throws MWException
+ * @return bool|int|string
+ */
+ public function getReaderIndex( $group = false, $wiki = false );
+
+ /**
+ * Set the master wait position
+ * If a DB_REPLICA connection has been opened already, waits
+ * Otherwise sets a variable telling it to wait if such a connection is opened
+ * @param DBMasterPos $pos
+ */
+ public function waitFor( $pos );
+
+ /**
+ * Set the master wait position and wait for a "generic" replica DB to catch up to it
+ *
+ * This can be used a faster proxy for waitForAll()
+ *
+ * @param DBMasterPos $pos
+ * @param int $timeout Max seconds to wait; default is mWaitTimeout
+ * @return bool Success (able to connect and no timeouts reached)
+ */
+ public function waitForOne( $pos, $timeout = null );
+
+ /**
+ * Set the master wait position and wait for ALL replica DBs to catch up to it
+ * @param DBMasterPos $pos
+ * @param int $timeout Max seconds to wait; default is mWaitTimeout
+ * @return bool Success (able to connect and no timeouts reached)
+ */
+ public function waitForAll( $pos, $timeout = null );
+
+ /**
+ * Get any open connection to a given server index, local or foreign
+ * Returns false if there is no connection open
+ *
+ * @param int $i Server index
+ * @return IDatabase|bool False on failure
+ */
+ public function getAnyOpenConnection( $i );
+
+ /**
+ * Get a connection by index
+ * This is the main entry point for this class.
+ *
+ * @param int $i Server index
+ * @param array|string|bool $groups Query group(s), or false for the generic reader
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ *
+ * @throws MWException
+ * @return IDatabase
+ */
+ public function getConnection( $i, $groups = [], $wiki = false );
+
+ /**
+ * Mark a foreign connection as being available for reuse under a different
+ * DB name or prefix. This mechanism is reference-counted, and must be called
+ * the same number of times as getConnection() to work.
+ *
+ * @param IDatabase $conn
+ * @throws MWException
+ */
+ public function reuseConnection( $conn );
+
+ /**
+ * Get a database connection handle reference
+ *
+ * The handle's methods wrap simply wrap those of a IDatabase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param int $db
+ * @param array|string|bool $groups Query group(s), or false for the generic reader
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @return DBConnRef
+ */
+ public function getConnectionRef( $db, $groups = [], $wiki = false );
+
+ /**
+ * Get a database connection handle reference without connecting yet
+ *
+ * The handle's methods wrap simply wrap those of a IDatabase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param int $db
+ * @param array|string|bool $groups Query group(s), or false for the generic reader
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @return DBConnRef
+ */
+ public function getLazyConnectionRef( $db, $groups = [], $wiki = false );
+
+ /**
+ * Open a connection to the server given by the specified index
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
+ *
+ * @param int $i Server index
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @return IDatabase|bool Returns false on errors
+ */
+ public function openConnection( $i, $wiki = false );
+
+ /**
+ * @return int
+ */
+ public function getWriterIndex();
+
+ /**
+ * Returns true if the specified index is a valid server index
+ *
+ * @param string $i
+ * @return bool
+ */
+ public function haveIndex( $i );
+
+ /**
+ * Returns true if the specified index is valid and has non-zero load
+ *
+ * @param string $i
+ * @return bool
+ */
+ public function isNonZeroLoad( $i );
+
+ /**
+ * Get the number of defined servers (not the number of open connections)
+ *
+ * @return int
+ */
+ public function getServerCount();
+
+ /**
+ * Get the host name or IP address of the server with the specified index
+ * Prefer a readable name if available.
+ * @param string $i
+ * @return string
+ */
+ public function getServerName( $i );
+
+ /**
+ * Return the server info structure for a given index, or false if the index is invalid.
+ * @param int $i
+ * @return array|bool
+ */
+ public function getServerInfo( $i );
+
+ /**
+ * Sets the server info structure for the given index. Entry at index $i
+ * is created if it doesn't exist
+ * @param int $i
+ * @param array $serverInfo
+ */
+ public function setServerInfo( $i, array $serverInfo );
+
+ /**
+ * Get the current master position for chronology control purposes
+ * @return DBMasterPos|bool Returns false if not applicable
+ */
+ public function getMasterPos();
+
+ /**
+ * Disable this load balancer. All connections are closed, and any attempt to
+ * open a new connection will result in a DBAccessError.
+ */
+ public function disable();
+
+ /**
+ * Close all open connections
+ */
+ public function closeAll();
+
+ /**
+ * Close a connection
+ *
+ * Using this function makes sure the LoadBalancer knows the connection is closed.
+ * If you use $conn->close() directly, the load balancer won't update its state.
+ *
+ * @param IDatabase $conn
+ */
+ public function closeConnection( IDatabase $conn );
+
+ /**
+ * Commit transactions on all open connections
+ * @param string $fname Caller name
+ * @throws DBExpectedError
+ */
+ public function commitAll( $fname = __METHOD__ );
+
+ /**
+ * Perform all pre-commit callbacks that remain part of the atomic transactions
+ * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
+ *
+ * Use this only for mutli-database commits
+ */
+ public function finalizeMasterChanges();
+
+ /**
+ * Perform all pre-commit checks for things like replication safety
+ *
+ * Use this only for mutli-database commits
+ *
+ * @param array $options Includes:
+ * - maxWriteDuration : max write query duration time in seconds
+ * @throws DBTransactionError
+ */
+ public function approveMasterChanges( array $options );
+
+ /**
+ * Flush any master transaction snapshots and set DBO_TRX (if DBO_DEFAULT is set)
+ *
+ * The DBO_TRX setting will be reverted to the default in each of these methods:
+ * - commitMasterChanges()
+ * - rollbackMasterChanges()
+ * - commitAll()
+ * This allows for custom transaction rounds from any outer transaction scope.
+ *
+ * @param string $fname
+ * @throws DBExpectedError
+ */
+ public function beginMasterChanges( $fname = __METHOD__ );
+
+ /**
+ * Issue COMMIT on all master connections where writes where done
+ * @param string $fname Caller name
+ * @throws DBExpectedError
+ */
+ public function commitMasterChanges( $fname = __METHOD__ );
+
+ /**
+ * Issue all pending post-COMMIT/ROLLBACK callbacks
+ *
+ * Use this only for mutli-database commits
+ *
+ * @param integer $type IDatabase::TRIGGER_* constant
+ * @return Exception|null The first exception or null if there were none
+ */
+ public function runMasterPostTrxCallbacks( $type );
+
+ /**
+ * Issue ROLLBACK only on master, only if queries were done on connection
+ * @param string $fname Caller name
+ * @throws DBExpectedError
+ */
+ public function rollbackMasterChanges( $fname = __METHOD__ );
+
+ /**
+ * Suppress all pending post-COMMIT/ROLLBACK callbacks
+ *
+ * Use this only for mutli-database commits
+ *
+ * @return Exception|null The first exception or null if there were none
+ */
+ public function suppressTransactionEndCallbacks();
+
+ /**
+ * Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshot
+ *
+ * @param string $fname Caller name
+ */
+ public function flushReplicaSnapshots( $fname = __METHOD__ );
+
+ /**
+ * @return bool Whether a master connection is already open
+ */
+ public function hasMasterConnection();
+
+ /**
+ * Determine if there are pending changes in a transaction by this thread
+ * @return bool
+ */
+ public function hasMasterChanges();
+
+ /**
+ * Get the timestamp of the latest write query done by this thread
+ * @return float|bool UNIX timestamp or false
+ */
+ public function lastMasterChangeTimestamp();
+
+ /**
+ * Check if this load balancer object had any recent or still
+ * pending writes issued against it by this PHP thread
+ *
+ * @param float $age How many seconds ago is "recent" [defaults to mWaitTimeout]
+ * @return bool
+ */
+ public function hasOrMadeRecentMasterChanges( $age = null );
+
+ /**
+ * Get the list of callers that have pending master changes
+ *
+ * @return string[] List of method names
+ */
+ public function pendingMasterChangeCallers();
+
+ /**
+ * @note This method will trigger a DB connection if not yet done
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @return bool Whether the generic connection for reads is highly "lagged"
+ */
+ public function getLaggedReplicaMode( $wiki = false );
+
+ /**
+ * @note This method will never cause a new DB connection
+ * @return bool Whether any generic connection used for reads was highly "lagged"
+ */
+ public function laggedReplicaUsed();
+
+ /**
+ * @note This method may trigger a DB connection if not yet done
+ * @param string|bool $wiki Wiki ID, or false for the current wiki
+ * @param IDatabase|null DB master connection; used to avoid loops [optional]
+ * @return string|bool Reason the master is read-only or false if it is not
+ */
+ public function getReadOnlyReason( $wiki = false, IDatabase $conn = null );
+
+ /**
+ * Disables/enables lag checks
+ * @param null|bool $mode
+ * @return bool
+ */
+ public function allowLagged( $mode = null );
+
+ /**
+ * @return bool
+ */
+ public function pingAll();
+
+ /**
+ * Call a function with each open connection object
+ * @param callable $callback
+ * @param array $params
+ */
+ public function forEachOpenConnection( $callback, array $params = [] );
+
+ /**
+ * Call a function with each open connection object to a master
+ * @param callable $callback
+ * @param array $params
+ */
+ public function forEachOpenMasterConnection( $callback, array $params = [] );
+
+ /**
+ * Call a function with each open replica DB connection object
+ * @param callable $callback
+ * @param array $params
+ */
+ public function forEachOpenReplicaConnection( $callback, array $params = [] );
+
+ /**
+ * Get the hostname and lag time of the most-lagged replica DB
+ *
+ * This is useful for maintenance scripts that need to throttle their updates.
+ * May attempt to open connections to replica DBs on the default DB. If there is
+ * no lag, the maximum lag will be reported as -1.
+ *
+ * @param bool|string $wiki Wiki ID, or false for the default database
+ * @return array ( host, max lag, index of max lagged host )
+ */
+ public function getMaxLag( $wiki = false );
+
+ /**
+ * Get an estimate of replication lag (in seconds) for each server
+ *
+ * Results are cached for a short time in memcached/process cache
+ *
+ * Values may be "false" if replication is too broken to estimate
+ *
+ * @param string|bool $wiki
+ * @return int[] Map of (server index => float|int|bool)
+ */
+ public function getLagTimes( $wiki = false );
+
+ /**
+ * Get the lag in seconds for a given connection, or zero if this load
+ * balancer does not have replication enabled.
+ *
+ * This should be used in preference to Database::getLag() in cases where
+ * replication may not be in use, since there is no way to determine if
+ * replication is in use at the connection level without running
+ * potentially restricted queries such as SHOW SLAVE STATUS. Using this
+ * function instead of Database::getLag() avoids a fatal error in this
+ * case on many installations.
+ *
+ * @param IDatabase $conn
+ * @return int|bool Returns false on error
+ */
+ public function safeGetLag( IDatabase $conn );
+
+ /**
+ * Wait for a replica DB to reach a specified master position
+ *
+ * This will connect to the master to get an accurate position if $pos is not given
+ *
+ * @param IDatabase $conn Replica DB
+ * @param DBMasterPos|bool $pos Master position; default: current position
+ * @param integer|null $timeout Timeout in seconds [optional]
+ * @return bool Success
+ */
+ public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null );
+
+ /**
+ * Clear the cache for slag lag delay times
+ *
+ * This is only used for testing
+ */
+ public function clearLagTimeCache();
+
+ /**
+ * Set a callback via IDatabase::setTransactionListener() on
+ * all current and future master connections of this load balancer
+ *
+ * @param string $name Callback name
+ * @param callable|null $callback
+ */
+ public function setTransactionListener( $name, callable $callback = null );
+}
* @ingroup Database
*/
+use Psr\Log\LoggerInterface;
use MediaWiki\MediaWikiServices;
use MediaWiki\Services\DestructibleService;
-use Psr\Log\LoggerInterface;
use MediaWiki\Logger\LoggerFactory;
/**
protected $trxProfiler;
/** @var LoggerInterface */
protected $trxLogger;
+ /** @var LoggerInterface */
+ protected $replLogger;
/** @var BagOStuff */
protected $srvCache;
+ /** @var BagOStuff */
+ protected $memCache;
/** @var WANObjectCache */
protected $wanCache;
$this->chronProt = $this->newChronologyProtector();
$this->trxProfiler = Profiler::instance()->getTransactionProfiler();
// Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
- $cache = ObjectCache::getLocalServerInstance();
- if ( $cache->getQoS( $cache::ATTR_EMULATION ) > $cache::QOS_EMULATION_SQL ) {
- $this->srvCache = $cache;
+ $sCache = ObjectCache::getLocalServerInstance();
+ if ( $sCache->getQoS( $sCache::ATTR_EMULATION ) > $sCache::QOS_EMULATION_SQL ) {
+ $this->srvCache = $sCache;
} else {
$this->srvCache = new EmptyBagOStuff();
}
+ $cCache = ObjectCache::getLocalClusterInstance();
+ if ( $cCache->getQoS( $cCache::ATTR_EMULATION ) > $cCache::QOS_EMULATION_SQL ) {
+ $this->memCache = $cCache;
+ } else {
+ $this->memCache = new EmptyBagOStuff();
+ }
$wCache = ObjectCache::getMainWANInstance();
if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
$this->wanCache = $wCache;
$this->wanCache = WANObjectCache::newEmpty();
}
$this->trxLogger = LoggerFactory::getInstance( 'DBTransaction' );
+ $this->replLogger = LoggerFactory::getInstance( 'DBReplication' );
$this->ticket = mt_rand();
}
* @deprecated since 1.27, use LBFactory::destroy()
*/
public static function destroyInstance() {
- self::singleton()->destroy();
+ MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
}
/**
'ip' => $request->getIP(),
'agent' => $request->getHeader( 'User-Agent' ),
],
- $request->getFloat( 'cpPosTime', null )
+ $request->getFloat( 'cpPosTime', $request->getCookie( 'cpPosTime', '' ) )
);
if ( PHP_SAPI === 'cli' ) {
$chronProt->setEnabled( false );
} );
}
+ /**
+ * Base parameters to LoadBalancer::__construct()
+ * @return array
+ */
+ final protected function baseLoadBalancerParams() {
+ return [
+ 'localDomain' => wfWikiID(),
+ 'readOnlyReason' => $this->readOnlyReason,
+ 'srvCache' => $this->srvCache,
+ 'memCache' => $this->memCache,
+ 'wanCache' => $this->wanCache,
+ 'trxProfiler' => $this->trxProfiler,
+ 'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
+ 'connLogger' => LoggerFactory::getInstance( 'DBConnection' ),
+ 'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
+ 'errorLogger' => [ MWExceptionHandler::class, 'logException' ]
+ ];
+ }
+
/**
* @param LoadBalancer $lb
*/
foreach ( $required as $key ) {
if ( !isset( $conf[$key] ) ) {
- throw new MWException( __CLASS__ . ": $key is required in configuration" );
+ throw new InvalidArgumentException( __CLASS__ . ": $key is required in configuration" );
}
$this->$key = $conf[$key];
}
*/
protected function newExternalLB( $cluster, $wiki = false ) {
if ( !isset( $this->externalLoads[$cluster] ) ) {
- throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+ throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
}
$template = $this->serverTemplate;
if ( isset( $this->externalTemplateOverrides ) ) {
* @return LoadBalancer
*/
private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
- $lb = new LoadBalancer( [
- 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $readOnlyReason,
- 'trxProfiler' => $this->trxProfiler,
- 'srvCache' => $this->srvCache,
- 'wanCache' => $this->wanCache
- ] );
-
+ $lb = new LoadBalancer( array_merge(
+ $this->baseLoadBalancerParams(),
+ [
+ 'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
+ 'loadMonitor' => $this->loadMonitorClass,
+ 'readOnlyReason' => $readOnlyReason
+ ]
+ ) );
$this->initLoadBalancer( $lb );
return $lb;
protected function newExternalLB( $cluster, $wiki = false ) {
global $wgExternalServers;
if ( !isset( $wgExternalServers[$cluster] ) ) {
- throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
+ throw new InvalidArgumentException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
}
return $this->newLoadBalancer( $wgExternalServers[$cluster] );
}
private function newLoadBalancer( array $servers ) {
- $lb = new LoadBalancer( [
- 'servers' => $servers,
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler,
- 'srvCache' => $this->srvCache,
- 'wanCache' => $this->wanCache
- ] );
-
+ $lb = new LoadBalancer( array_merge(
+ $this->baseLoadBalancerParams(),
+ [
+ 'servers' => $servers,
+ 'loadMonitor' => $this->loadMonitorClass,
+ ]
+ ) );
$this->initLoadBalancer( $lb );
return $lb;
public function __construct( array $conf ) {
parent::__construct( $conf );
- $this->lb = new LoadBalancerSingle( [
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler,
- 'srvCache' => $this->srvCache,
- 'wanCache' => $this->wanCache
- ] + $conf );
+ $this->lb = new LoadBalancerSingle( array_merge( $this->baseLoadBalancerParams(), $conf ) );
}
/**
<?php
/**
- * Database load balancing.
+ * Database load balancing manager
*
* 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
* @file
* @ingroup Database
*/
+use Psr\Log\LoggerInterface;
/**
- * Database load balancing object
+ * Database load balancing, tracking, and transaction management object
*
- * @todo document
* @ingroup Database
*/
-class LoadBalancer {
+class LoadBalancer implements ILoadBalancer {
/** @var array[] Map of (server index => server config array) */
private $mServers;
/** @var array[] Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */
private $mAllowLagged;
/** @var integer Seconds to spend waiting on replica DB lag to resolve */
private $mWaitTimeout;
- /** @var array LBFactory information */
- private $mParentInfo;
-
/** @var string The LoadMonitor subclass name */
private $mLoadMonitorClass;
+
/** @var LoadMonitor */
private $mLoadMonitor;
/** @var BagOStuff */
private $srvCache;
+ /** @var BagOStuff */
+ private $memCache;
/** @var WANObjectCache */
private $wanCache;
/** @var TransactionProfiler */
protected $trxProfiler;
+ /** @var LoggerInterface */
+ protected $replLogger;
+ /** @var LoggerInterface */
+ protected $connLogger;
+ /** @var LoggerInterface */
+ protected $queryLogger;
+ /** @var LoggerInterface */
+ protected $perfLogger;
/** @var bool|DatabaseBase Database connection that caused a problem */
private $mErrorConnection;
private $trxRoundId = false;
/** @var array[] Map of (name => callable) */
private $trxRecurringCallbacks = [];
+ /** @var string Local Wiki ID and default for selectDB() calls */
+ private $localDomain;
+ /** @var callable Exception logger */
+ private $errorLogger;
+
+ /** @var boolean */
+ private $disabled = false;
/** @var integer Warn when this many connection are held */
const CONN_HELD_WARN_THRESHOLD = 10;
/** @var integer Seconds to cache master server read-only status */
const TTL_CACHE_READONLY = 5;
- /**
- * @var boolean
- */
- private $disabled = false;
-
- /**
- * @param array $params Array with keys:
- * - servers : Required. Array of server info structures.
- * - loadMonitor : Name of a class used to fetch server lag and load.
- * - readOnlyReason : Reason the master DB is read-only if so [optional]
- * - waitTimeout : Maximum time to wait for replicas for consistency [optional]
- * - srvCache : BagOStuff object [optional]
- * - wanCache : WANObjectCache object [optional]
- * @throws MWException
- */
public function __construct( array $params ) {
if ( !isset( $params['servers'] ) ) {
- throw new MWException( __CLASS__ . ': missing servers parameter' );
+ throw new InvalidArgumentException( __CLASS__ . ': missing servers parameter' );
}
$this->mServers = $params['servers'];
$this->mWaitTimeout = isset( $params['waitTimeout'] )
? $params['waitTimeout']
: self::POS_WAIT_TIMEOUT;
+ $this->localDomain = isset( $params['localDomain'] ) ? $params['localDomain'] : '';
$this->mReadIndex = -1;
- $this->mWriteIndex = -1;
$this->mConns = [
'local' => [],
'foreignUsed' => [],
} else {
$this->srvCache = new EmptyBagOStuff();
}
+ if ( isset( $params['memCache'] ) ) {
+ $this->memCache = $params['memCache'];
+ } else {
+ $this->memCache = new EmptyBagOStuff();
+ }
if ( isset( $params['wanCache'] ) ) {
$this->wanCache = $params['wanCache'];
} else {
} else {
$this->trxProfiler = new TransactionProfiler();
}
+
+ $this->errorLogger = isset( $params['errorLogger'] )
+ ? $params['errorLogger']
+ : function ( Exception $e ) {
+ trigger_error( E_WARNING, $e->getMessage() );
+ };
+
+ foreach ( [ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ] as $key ) {
+ $this->$key = isset( $params[$key] ) ? $params[$key] : new \Psr\Log\NullLogger();
+ }
}
/**
private function getLoadMonitor() {
if ( !isset( $this->mLoadMonitor ) ) {
$class = $this->mLoadMonitorClass;
- $this->mLoadMonitor = new $class( $this );
+ $this->mLoadMonitor = new $class( $this, $this->srvCache, $this->memCache );
+ $this->mLoadMonitor->setLogger( $this->replLogger );
}
return $this->mLoadMonitor;
$host = $this->getServerName( $i );
if ( $lag === false && !is_infinite( $maxServerLag ) ) {
- wfDebugLog( 'replication', "Server $host (#$i) is not replicating?" );
+ $this->replLogger->error( "Server $host (#$i) is not replicating?" );
unset( $loads[$i] );
} elseif ( $lag > $maxServerLag ) {
- wfDebugLog( 'replication', "Server $host (#$i) has >= $lag seconds of lag" );
+ $this->replLogger->warning( "Server $host (#$i) has >= $lag seconds of lag" );
unset( $loads[$i] );
}
}
return ArrayUtils::pickRandom( $loads );
}
- /**
- * Get the index of the reader connection, which may be a replica DB
- * This takes into account load ratios and lag times. It should
- * always return a consistent index during a given invocation
- *
- * Side effect: opens connections to databases
- * @param string|bool $group Query group, or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @throws MWException
- * @return bool|int|string
- */
public function getReaderIndex( $group = false, $wiki = false ) {
- global $wgDBtype;
-
- # @todo FIXME: For now, only go through all this for mysql databases
- if ( $wgDBtype != 'mysql' ) {
- return $this->getWriterIndex();
- }
-
if ( count( $this->mServers ) == 1 ) {
# Skip the load balancing if there's only one server
- return 0;
+ return $this->getWriterIndex();
} elseif ( $group === false && $this->mReadIndex >= 0 ) {
# Shortcut if generic reader exists already
return $this->mReadIndex;
$nonErrorLoads = $this->mGroupLoads[$group];
} else {
# No loads for this group, return false and the caller can use some other group
- wfDebugLog( 'connect', __METHOD__ . ": no loads for group $group\n" );
+ $this->connLogger->info( __METHOD__ . ": no loads for group $group" );
return false;
}
}
if ( !count( $nonErrorLoads ) ) {
- throw new MWException( "Empty server array given to LoadBalancer" );
+ throw new InvalidArgumentException( "Empty server array given to LoadBalancer" );
}
- # Scale the configured load ratios according to the dynamic load (if the load monitor supports it)
+ # Scale the configured load ratios according to the dynamic load if supported
$this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki );
$laggedReplicaMode = false;
}
if ( $i === false && count( $currentLoads ) != 0 ) {
# All replica DBs lagged. Switch to read-only mode
- wfDebugLog( 'replication', "All replica DBs lagged. Switch to read-only mode" );
+ $this->replLogger->error( "All replica DBs lagged. Switch to read-only mode" );
$i = ArrayUtils::pickRandom( $currentLoads );
$laggedReplicaMode = true;
}
# pickRandom() returned false
# This is permanent and means the configuration or the load monitor
# wants us to return false.
- wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" );
+ $this->connLogger->debug( __METHOD__ . ": pickRandom() returned false" );
return false;
}
$serverName = $this->getServerName( $i );
- wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: $serverName..." );
+ $this->connLogger->debug( __METHOD__ . ": Using reader #$i: $serverName..." );
$conn = $this->openConnection( $i, $wiki );
if ( !$conn ) {
- wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" );
+ $this->connLogger->warning( __METHOD__ . ": Failed connecting to $i/$wiki" );
unset( $nonErrorLoads[$i] );
unset( $currentLoads[$i] );
$i = false;
# If all servers were down, quit now
if ( !count( $nonErrorLoads ) ) {
- wfDebugLog( 'connect', "All servers down" );
+ $this->connLogger->error( "All servers down" );
}
if ( $i !== false ) {
}
}
$serverName = $this->getServerName( $i );
- wfDebugLog( 'connect', __METHOD__ .
- ": using server $serverName for group '$group'\n" );
+ $this->connLogger->debug(
+ __METHOD__ . ": using server $serverName for group '$group'\n" );
}
return $i;
}
- /**
- * Set the master wait position
- * If a DB_REPLICA connection has been opened already, waits
- * Otherwise sets a variable telling it to wait if such a connection is opened
- * @param DBMasterPos $pos
- */
public function waitFor( $pos ) {
$this->mWaitForPos = $pos;
$i = $this->mReadIndex;
return $ok;
}
- /**
- * Set the master wait position and wait for ALL replica DBs to catch up to it
- * @param DBMasterPos $pos
- * @param int $timeout Max seconds to wait; default is mWaitTimeout
- * @return bool Success (able to connect and no timeouts reached)
- */
public function waitForAll( $pos, $timeout = null ) {
$this->mWaitForPos = $pos;
$serverCount = count( $this->mServers );
return $ok;
}
- /**
- * Get any open connection to a given server index, local or foreign
- * Returns false if there is no connection open
- *
- * @param int $i Server index
- * @return DatabaseBase|bool False on failure
- */
public function getAnyOpenConnection( $i ) {
foreach ( $this->mConns as $connsByServer ) {
if ( !empty( $connsByServer[$i] ) ) {
/** @var DBMasterPos $knownReachedPos */
$knownReachedPos = $this->srvCache->get( $key );
if ( $knownReachedPos && $knownReachedPos->hasReached( $this->mWaitForPos ) ) {
- wfDebugLog( 'replication', __METHOD__ .
+ $this->replLogger->debug( __METHOD__ .
": replica DB $server known to be caught up (pos >= $knownReachedPos).\n" );
return true;
}
$conn = $this->getAnyOpenConnection( $index );
if ( !$conn ) {
if ( !$open ) {
- wfDebugLog( 'replication', __METHOD__ . ": no connection open for $server\n" );
+ $this->replLogger->debug( __METHOD__ . ": no connection open for $server\n" );
return false;
} else {
$conn = $this->openConnection( $index, '' );
if ( !$conn ) {
- wfDebugLog( 'replication', __METHOD__ . ": failed to connect to $server\n" );
+ $this->replLogger->warning( __METHOD__ . ": failed to connect to $server\n" );
return false;
}
}
}
- wfDebugLog( 'replication', __METHOD__ . ": Waiting for replica DB $server to catch up...\n" );
+ $this->replLogger->info( __METHOD__ . ": Waiting for replica DB $server to catch up...\n" );
$timeout = $timeout ?: $this->mWaitTimeout;
$result = $conn->masterPosWait( $this->mWaitForPos, $timeout );
if ( $result == -1 || is_null( $result ) ) {
// Timed out waiting for replica DB, use master instead
$msg = __METHOD__ . ": Timed out waiting on $server pos {$this->mWaitForPos}";
- wfDebugLog( 'replication', "$msg\n" );
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+ $this->replLogger->warning( "$msg\n" );
+ $this->perfLogger->warning( "$msg:\n" . wfBacktrace( true ) );
$ok = false;
} else {
- wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
+ $this->replLogger->info( __METHOD__ . ": Done\n" );
$ok = true;
// Remember that the DB reached this point
$this->srvCache->set( $key, $this->mWaitForPos, BagOStuff::TTL_DAY );
return $ok;
}
- /**
- * Get a connection by index
- * This is the main entry point for this class.
- *
- * @param int $i Server index
- * @param array|string|bool $groups Query group(s), or false for the generic reader
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- *
- * @throws MWException
- * @return DatabaseBase
- */
public function getConnection( $i, $groups = [], $wiki = false ) {
if ( $i === null || $i === false ) {
- throw new MWException( 'Attempt to call ' . __METHOD__ .
+ throw new InvalidArgumentException( 'Attempt to call ' . __METHOD__ .
' with invalid server index' );
}
- if ( $wiki === wfWikiID() ) {
+ if ( $wiki === $this->localDomain ) {
$wiki = false;
}
if ( $this->connsOpened > $oldConnsOpened ) {
$host = $conn->getServer();
$dbname = $conn->getDBname();
- $trxProf = Profiler::instance()->getTransactionProfiler();
- $trxProf->recordConnection( $host, $dbname, $masterOnly );
+ $this->trxProfiler->recordConnection( $host, $dbname, $masterOnly );
}
if ( $masterOnly ) {
return $conn;
}
- /**
- * Mark a foreign connection as being available for reuse under a different
- * DB name or prefix. This mechanism is reference-counted, and must be called
- * the same number of times as getConnection() to work.
- *
- * @param DatabaseBase $conn
- * @throws MWException
- */
public function reuseConnection( $conn ) {
$serverIndex = $conn->getLBInfo( 'serverIndex' );
$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
$wiki = $dbName;
}
if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
- throw new MWException( __METHOD__ . ": connection not found, has " .
+ throw new InvalidArgumentException( __METHOD__ . ": connection not found, has " .
"the connection been freed already?" );
}
$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return DBConnRef
+ * @since 1.22
*/
public function getConnectionRef( $db, $groups = [], $wiki = false ) {
return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
* @param array|string|bool $groups Query group(s), or false for the generic reader
* @param string|bool $wiki Wiki ID, or false for the current wiki
* @return DBConnRef
+ * @since 1.22
*/
public function getLazyConnectionRef( $db, $groups = [], $wiki = false ) {
+ $wiki = ( $wiki !== false ) ? $wiki : $this->localDomain;
+
return new DBConnRef( $this, [ $db, $groups, $wiki ] );
}
- /**
- * Open a connection to the server given by the specified index
- * Index must be an actual index into the array.
- * If the server is already open, returns it.
- *
- * On error, returns false, and the connection which caused the
- * error will be available via $this->mErrorConnection.
- *
- * @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
- *
- * @param int $i Server index
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return DatabaseBase|bool Returns false on errors
- */
public function openConnection( $i, $wiki = false ) {
if ( $wiki !== false ) {
$conn = $this->openForeignConnection( $i, $wiki );
$conn = $this->reallyOpenConnection( $server, false );
$serverName = $this->getServerName( $i );
if ( $conn->isOpen() ) {
- wfDebugLog( 'connect', "Connected to database $i at $serverName\n" );
+ $this->connLogger->debug( "Connected to database $i at '$serverName'." );
$this->mConns['local'][$i][0] = $conn;
} else {
- wfDebugLog( 'connect', "Failed to connect to database $i at $serverName\n" );
+ $this->connLogger->warning( "Failed to connect to database $i at '$serverName'." );
$this->mErrorConnection = $conn;
$conn = false;
}
* @return DatabaseBase
*/
private function openForeignConnection( $i, $wiki ) {
- list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+ list( $dbName, $prefix ) = explode( '-', $wiki, 2 ) + [ '', '' ];
+
if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
// Reuse an already-used connection
$conn = $this->mConns['foreignUsed'][$i][$wiki];
}
if ( !is_array( $server ) ) {
- throw new MWException( 'You must update your load-balancing configuration. ' .
+ throw new InvalidArgumentException( 'You must update your load-balancing configuration. ' .
'See DefaultSettings.php entry for $wgDBservers.' );
}
// Log when many connection are made on requests
if ( ++$this->connsOpened >= self::CONN_HELD_WARN_THRESHOLD ) {
- wfDebugLog( 'DBPerformance', __METHOD__ . ": " .
+ $this->perfLogger->warning( __METHOD__ . ": " .
"{$this->connsOpened}+ connections made (master=$masterName)\n" .
wfBacktrace( true ) );
}
- # Create object
+ // Set loggers
+ $server['connLogger'] = $this->connLogger;
+ $server['queryLogger'] = $this->queryLogger;
+
+ // Create a live connection object
try {
$db = DatabaseBase::factory( $server['type'], $server );
} catch ( DBConnectionError $e ) {
return false; /* not reached */
}
- /**
- * @return int
- * @since 1.26
- */
public function getWriterIndex() {
return 0;
}
- /**
- * Returns true if the specified index is a valid server index
- *
- * @param string $i
- * @return bool
- */
public function haveIndex( $i ) {
return array_key_exists( $i, $this->mServers );
}
- /**
- * Returns true if the specified index is valid and has non-zero load
- *
- * @param string $i
- * @return bool
- */
public function isNonZeroLoad( $i ) {
return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
}
- /**
- * Get the number of defined servers (not the number of open connections)
- *
- * @return int
- */
public function getServerCount() {
return count( $this->mServers );
}
- /**
- * Get the host name or IP address of the server with the specified index
- * Prefer a readable name if available.
- * @param string $i
- * @return string
- */
public function getServerName( $i ) {
if ( isset( $this->mServers[$i]['hostName'] ) ) {
$name = $this->mServers[$i]['hostName'];
return ( $name != '' ) ? $name : 'localhost';
}
- /**
- * Return the server info structure for a given index, or false if the index is invalid.
- * @param int $i
- * @return array|bool
- */
public function getServerInfo( $i ) {
if ( isset( $this->mServers[$i] ) ) {
return $this->mServers[$i];
}
}
- /**
- * Sets the server info structure for the given index. Entry at index $i
- * is created if it doesn't exist
- * @param int $i
- * @param array $serverInfo
- */
public function setServerInfo( $i, array $serverInfo ) {
$this->mServers[$i] = $serverInfo;
}
- /**
- * Get the current master position for chronology control purposes
- * @return DBMasterPos|bool Returns false if not applicable
- */
public function getMasterPos() {
# If this entire request was served from a replica DB without opening a connection to the
# master (however unlikely that may be), then we can fetch the position from the replica DB.
$this->disabled = true;
}
- /**
- * Close all open connections
- */
public function closeAll() {
$this->forEachOpenConnection( function ( DatabaseBase $conn ) {
$conn->close();
$this->connsOpened = 0;
}
- /**
- * Close a connection
- *
- * Using this function makes sure the LoadBalancer knows the connection is closed.
- * If you use $conn->close() directly, the load balancer won't update its state.
- *
- * @param DatabaseBase $conn
- */
- public function closeConnection( DatabaseBase $conn ) {
+ public function closeConnection( IDatabase $conn ) {
$serverIndex = $conn->getLBInfo( 'serverIndex' ); // second index level of mConns
foreach ( $this->mConns as $type => $connsByServer ) {
if ( !isset( $connsByServer[$serverIndex] ) ) {
$conn->close();
}
- /**
- * Commit transactions on all open connections
- * @param string $fname Caller name
- * @throws DBExpectedError
- */
public function commitAll( $fname = __METHOD__ ) {
$failures = [];
try {
$conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
} catch ( DBError $e ) {
- MWExceptionHandler::logException( $e );
+ call_user_func( $this->errorLogger, $e );
$failures[] = "{$conn->getServer()}: {$e->getMessage()}";
}
if ( $restore && $conn->getLBInfo( 'master' ) ) {
try {
$conn->flushSnapshot( $fname );
} catch ( DBError $e ) {
- MWExceptionHandler::logException( $e );
+ call_user_func( $this->errorLogger, $e );
$failures[] = "{$conn->getServer()}: {$e->getMessage()}";
}
$conn->setTrxEndCallbackSuppression( false );
}
}
- /**
- * Issue COMMIT on all master connections where writes where done
- * @param string $fname Caller name
- * @throws DBExpectedError
- */
public function commitMasterChanges( $fname = __METHOD__ ) {
$failures = [];
$conn->flushSnapshot( $fname );
}
} catch ( DBError $e ) {
- MWExceptionHandler::logException( $e );
+ call_user_func( $this->errorLogger, $e );
$failures[] = "{$conn->getServer()}: {$e->getMessage()}";
}
if ( $restore ) {
}
/**
- * @param DatabaseBase $conn
+ * @param IDatabase $conn
*/
- private function applyTransactionRoundFlags( DatabaseBase $conn ) {
+ private function applyTransactionRoundFlags( IDatabase $conn ) {
if ( $conn->getFlag( DBO_DEFAULT ) ) {
// DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
// Force DBO_TRX even in CLI mode since a commit round is expected soon.
}
/**
- * @param DatabaseBase $conn
+ * @param IDatabase $conn
*/
- private function undoTransactionRoundFlags( DatabaseBase $conn ) {
+ private function undoTransactionRoundFlags( IDatabase $conn ) {
if ( $conn->getFlag( DBO_DEFAULT ) ) {
$conn->restoreFlags( $conn::RESTORE_PRIOR );
}
return $fnames;
}
- /**
- * @note This method will trigger a DB connection if not yet done
- * @param string|bool $wiki Wiki ID, or false for the current wiki
- * @return bool Whether the generic connection for reads is highly "lagged"
- */
public function getLaggedReplicaMode( $wiki = false ) {
// No-op if there is only one DB (also avoids recursion)
if ( !$this->laggedReplicaMode && $this->getServerCount() > 1 ) {
/**
* @note This method may trigger a DB connection if not yet done
* @param string|bool $wiki Wiki ID, or false for the current wiki
- * @param DatabaseBase|null DB master connection; used to avoid loops [optional]
+ * @param IDatabase|null DB master connection; used to avoid loops [optional]
* @return string|bool Reason the master is read-only or false if it is not
* @since 1.27
*/
- public function getReadOnlyReason( $wiki = false, DatabaseBase $conn = null ) {
+ public function getReadOnlyReason( $wiki = false, IDatabase $conn = null ) {
if ( $this->readOnlyReason !== false ) {
return $this->readOnlyReason;
} elseif ( $this->getLaggedReplicaMode( $wiki ) ) {
/**
* @param string $wiki Wiki ID, or false for the current wiki
- * @param DatabaseBase|null DB master connectionl used to avoid loops [optional]
+ * @param IDatabase|null DB master connectionl used to avoid loops [optional]
* @return bool
*/
- private function masterRunningReadOnly( $wiki, DatabaseBase $conn = null ) {
+ private function masterRunningReadOnly( $wiki, IDatabase $conn = null ) {
$cache = $this->wanCache;
$masterServer = $this->getServerName( $this->getWriterIndex() );
);
}
- /**
- * Disables/enables lag checks
- * @param null|bool $mode
- * @return bool
- */
public function allowLagged( $mode = null ) {
if ( $mode === null ) {
return $this->mAllowLagged;
return $this->mAllowLagged;
}
- /**
- * @return bool
- */
public function pingAll() {
$success = true;
$this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( &$success ) {
return $success;
}
- /**
- * Call a function with each open connection object
- * @param callable $callback
- * @param array $params
- */
public function forEachOpenConnection( $callback, array $params = [] ) {
foreach ( $this->mConns as $connsByServer ) {
foreach ( $connsByServer as $serverConns ) {
}
}
- /**
- * Get the hostname and lag time of the most-lagged replica DB
- *
- * This is useful for maintenance scripts that need to throttle their updates.
- * May attempt to open connections to replica DBs on the default DB. If there is
- * no lag, the maximum lag will be reported as -1.
- *
- * @param bool|string $wiki Wiki ID, or false for the default database
- * @return array ( host, max lag, index of max lagged host )
- */
public function getMaxLag( $wiki = false ) {
$maxLag = -1;
$host = '';
return [ $host, $maxLag, $maxIndex ];
}
- /**
- * Get an estimate of replication lag (in seconds) for each server
- *
- * Results are cached for a short time in memcached/process cache
- *
- * Values may be "false" if replication is too broken to estimate
- *
- * @param string|bool $wiki
- * @return int[] Map of (server index => float|int|bool)
- */
public function getLagTimes( $wiki = false ) {
if ( $this->getServerCount() <= 1 ) {
return [ 0 => 0 ]; // no replication = no lag
return $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
}
- /**
- * Get the lag in seconds for a given connection, or zero if this load
- * balancer does not have replication enabled.
- *
- * This should be used in preference to Database::getLag() in cases where
- * replication may not be in use, since there is no way to determine if
- * replication is in use at the connection level without running
- * potentially restricted queries such as SHOW SLAVE STATUS. Using this
- * function instead of Database::getLag() avoids a fatal error in this
- * case on many installations.
- *
- * @param IDatabase $conn
- * @return int|bool Returns false on error
- */
public function safeGetLag( IDatabase $conn ) {
if ( $this->getServerCount() == 1 ) {
return 0;
*
* @param IDatabase $conn Replica DB
* @param DBMasterPos|bool $pos Master position; default: current position
- * @param integer|null $timeout Timeout in seconds [optional]
+ * @param integer $timeout Timeout in seconds
* @return bool Success
* @since 1.27
*/
- public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
+ public function safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = 10 ) {
if ( $this->getServerCount() == 1 || !$conn->getLBInfo( 'replica' ) ) {
return true; // server is not a replica DB
}
return false; // something is misconfigured
}
- $timeout = $timeout ?: $this->mWaitTimeout;
$result = $conn->masterPosWait( $pos, $timeout );
if ( $result == -1 || is_null( $result ) ) {
$msg = __METHOD__ . ": Timed out waiting on {$conn->getServer()} pos {$pos}";
- wfDebugLog( 'replication', "$msg\n" );
- wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
+ $this->replLogger->warning( "$msg\n" );
+ $this->perfLogger->warning( "$msg:\n" . wfBacktrace( true ) );
$ok = false;
} else {
- wfDebugLog( 'replication', __METHOD__ . ": Done\n" );
+ $this->replLogger->info( __METHOD__ . ": Done\n" );
$ok = true;
}
* Clear the cache for slag lag delay times
*
* This is only used for testing
+ * @since 1.26
*/
public function clearLagTimeCache() {
$this->getLoadMonitor()->clearCaches();
}
);
}
+
+ /**
+ * Set a new table prefix for the existing local wiki ID for testing
+ *
+ * @param string $prefix
+ * @since 1.28
+ */
+ public function setDomainPrefix( $prefix ) {
+ list( $dbName, ) = explode( '-', $this->localDomain, 2 );
+
+ $this->localDomain = "{$dbName}-{$prefix}";
+ }
}
+++ /dev/null
-<?php
-/**
- * Database load monitoring.
- *
- * 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
- * @ingroup Database
- */
-
-/**
- * An interface for database load monitoring
- *
- * @ingroup Database
- */
-interface LoadMonitor {
- /**
- * Construct a new LoadMonitor with a given LoadBalancer parent
- *
- * @param LoadBalancer $parent
- */
- public function __construct( $parent );
-
- /**
- * Perform pre-connection load ratio adjustment.
- * @param array &$loads
- * @param string|bool $group The selected query group. Default: false
- * @param string|bool $wiki Default: false
- */
- public function scaleLoads( &$loads, $group = false, $wiki = false );
-
- /**
- * Get an estimate of replication lag (in seconds) for each server
- *
- * Values may be "false" if replication is too broken to estimate
- *
- * @param array $serverIndexes
- * @param string $wiki
- *
- * @return array Map of (server index => float|int|bool)
- */
- public function getLagTimes( $serverIndexes, $wiki );
-
- /**
- * Clear any process and persistent cache of lag times
- * @since 1.27
- */
- public function clearCaches();
-}
-
-class LoadMonitorNull implements LoadMonitor {
- public function __construct( $parent ) {
- }
-
- public function scaleLoads( &$loads, $group = false, $wiki = false ) {
- }
-
- public function getLagTimes( $serverIndexes, $wiki ) {
- return array_fill_keys( $serverIndexes, 0 );
- }
-
- public function clearCaches() {
-
- }
-}
+++ /dev/null
-<?php
-/**
- * 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
- * @ingroup Database
- */
-
-/**
- * Basic MySQL load monitor with no external dependencies
- * Uses memcached to cache the replication lag for a short time
- *
- * @ingroup Database
- */
-class LoadMonitorMySQL implements LoadMonitor {
- /** @var LoadBalancer */
- public $parent;
- /** @var BagOStuff */
- protected $srvCache;
- /** @var BagOStuff */
- protected $mainCache;
-
- public function __construct( $parent ) {
- $this->parent = $parent;
-
- $this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
- $this->mainCache = ObjectCache::getLocalClusterInstance();
- }
-
- public function scaleLoads( &$loads, $group = false, $wiki = false ) {
- }
-
- public function getLagTimes( $serverIndexes, $wiki ) {
- if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
- # Single server only, just return zero without caching
- return [ 0 => 0 ];
- }
-
- $key = $this->getLagTimeCacheKey();
- # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
- $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
- # Keep keys around longer as fallbacks
- $staleTTL = 60;
-
- # (a) Check the local APC cache
- $value = $this->srvCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from local cache" );
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: false;
-
- # (b) Check the shared cache and backfill APC
- $value = $this->mainCache->get( $key );
- if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
- $this->srvCache->set( $key, $value, $staleTTL );
- wfDebugLog( 'replication', __METHOD__ . ": got lag times ($key) from main cache" );
-
- return $value['lagTimes']; // cache hit
- }
- $staleValue = $value ?: $staleValue;
-
- # (c) Cache key missing or expired; regenerate and backfill
- if ( $this->mainCache->lock( $key, 0, 10 ) ) {
- # Let this process alone update the cache value
- $cache = $this->mainCache;
- /** @noinspection PhpUnusedLocalVariableInspection */
- $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
- $cache->unlock( $key );
- } );
- } elseif ( $staleValue ) {
- # Could not acquire lock but an old cache exists, so use it
- return $staleValue['lagTimes'];
- }
-
- $lagTimes = [];
- foreach ( $serverIndexes as $i ) {
- if ( $i == $this->parent->getWriterIndex() ) {
- $lagTimes[$i] = 0; // master always has no lag
- continue;
- }
-
- $conn = $this->parent->getAnyOpenConnection( $i );
- if ( $conn ) {
- $close = false; // already open
- } else {
- $conn = $this->parent->openConnection( $i, $wiki );
- $close = true; // new connection
- }
-
- if ( !$conn ) {
- $lagTimes[$i] = false;
- $host = $this->parent->getServerName( $i );
- wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is unreachable" );
- continue;
- }
-
- $lagTimes[$i] = $conn->getLag();
- if ( $lagTimes[$i] === false ) {
- $host = $this->parent->getServerName( $i );
- wfDebugLog( 'replication', __METHOD__ . ": host $host (#$i) is not replicating?" );
- }
-
- if ( $close ) {
- # Close the connection to avoid sleeper connections piling up.
- # Note that the caller will pick one of these DBs and reconnect,
- # which is slightly inefficient, but this only matters for the lag
- # time cache miss cache, which is far less common that cache hits.
- $this->parent->closeConnection( $conn );
- }
- }
-
- # Add a timestamp key so we know when it was cached
- $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
- $this->mainCache->set( $key, $value, $staleTTL );
- $this->srvCache->set( $key, $value, $staleTTL );
- wfDebugLog( 'replication', __METHOD__ . ": re-calculated lag times ($key)" );
-
- return $value['lagTimes'];
- }
-
- public function clearCaches() {
- $key = $this->getLagTimeCacheKey();
- $this->srvCache->delete( $key );
- $this->mainCache->delete( $key );
- }
-
- private function getLagTimeCacheKey() {
- $writerIndex = $this->parent->getWriterIndex();
- // Lag is per-server, not per-DB, so key on the master DB name
- return $this->srvCache->makeGlobalKey(
- 'lag-times', $this->parent->getServerName( $writerIndex )
- );
- }
-}
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
$status = false;
$error = get_class( $e ) . ': ' . $e->getMessage();
- MWExceptionHandler::logException( $e );
}
// Always attempt to call teardown() even if Job throws exception.
try {
// Use a named lock so that jobs for this page see each others' changes
$lockKey = "CategoryMembershipUpdates:{$page->getId()}";
- $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
+ $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
if ( !$scopedLock ) {
$this->setLastError( "Could not acquire lock '$lockKey'" );
return false;
--- /dev/null
+<?php
+/**
+ * Object caching using memcached.
+ *
+ * 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
+ * @ingroup Cache
+ */
+
+/**
+ * A wrapper class for the PECL memcached client
+ *
+ * @ingroup Cache
+ */
+class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
+
+ /**
+ * Constructor
+ *
+ * Available parameters are:
+ * - servers: The list of IP:port combinations holding the memcached servers.
+ * - persistent: Whether to use a persistent connection
+ * - compress_threshold: The minimum size an object must be before it is compressed
+ * - timeout: The read timeout in microseconds
+ * - connect_timeout: The connect timeout in seconds
+ * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
+ * - server_failure_limit: Limit for server connect failures before it is removed
+ * - serializer: May be either "php" or "igbinary". Igbinary produces more compact
+ * values, but serialization is much slower unless the php.ini option
+ * igbinary.compact_strings is off.
+ * - use_binary_protocol Whether to enable the binary protocol (default is ASCII) (boolean)
+ * @param array $params
+ * @throws InvalidArgumentException
+ */
+ function __construct( $params ) {
+ parent::__construct( $params );
+ $params = $this->applyDefaultParams( $params );
+
+ if ( $params['persistent'] ) {
+ // The pool ID must be unique to the server/option combination.
+ // The Memcached object is essentially shared for each pool ID.
+ // We can only reuse a pool ID if we keep the config consistent.
+ $this->client = new Memcached( md5( serialize( $params ) ) );
+ if ( count( $this->client->getServerList() ) ) {
+ $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
+ return; // already initialized; don't add duplicate servers
+ }
+ } else {
+ $this->client = new Memcached;
+ }
+
+ if ( $params['use_binary_protocol'] ) {
+ $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
+ }
+
+ if ( isset( $params['retry_timeout'] ) ) {
+ $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
+ }
+
+ if ( isset( $params['server_failure_limit'] ) ) {
+ $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
+ }
+
+ // The compression threshold is an undocumented php.ini option for some
+ // reason. There's probably not much harm in setting it globally, for
+ // compatibility with the settings for the PHP client.
+ ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
+
+ // Set timeouts
+ $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
+ $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
+ $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
+ $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
+
+ // Set libketama mode since it's recommended by the documentation and
+ // is as good as any. There's no way to configure libmemcached to use
+ // hashes identical to the ones currently in use by the PHP client, and
+ // even implementing one of the libmemcached hashes in pure PHP for
+ // forwards compatibility would require MemcachedClient::get_sock() to be
+ // rewritten.
+ $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
+
+ // Set the serializer
+ switch ( $params['serializer'] ) {
+ case 'php':
+ $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
+ break;
+ case 'igbinary':
+ if ( !Memcached::HAVE_IGBINARY ) {
+ throw new InvalidArgumentException(
+ __CLASS__ . ': the igbinary extension is not available ' .
+ 'but igbinary serialization was requested.'
+ );
+ }
+ $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
+ break;
+ default:
+ throw new InvalidArgumentException(
+ __CLASS__ . ': invalid value for serializer parameter'
+ );
+ }
+ $servers = [];
+ foreach ( $params['servers'] as $host ) {
+ if ( preg_match( '/^\[(.+)\]:(\d+)$/', $host, $m ) ) {
+ $servers[] = [ $m[1], (int)$m[2] ]; // (ip, port)
+ } elseif ( preg_match( '/^([^:]+):(\d+)$/', $host, $m ) ) {
+ $servers[] = [ $m[1], (int)$m[2] ]; // (ip or path, port)
+ } else {
+ $servers[] = [ $host, false ]; // (ip or path, port)
+ }
+ }
+ $this->client->addServers( $servers );
+ }
+
+ protected function applyDefaultParams( $params ) {
+ $params = parent::applyDefaultParams( $params );
+
+ if ( !isset( $params['use_binary_protocol'] ) ) {
+ $params['use_binary_protocol'] = false;
+ }
+
+ if ( !isset( $params['serializer'] ) ) {
+ $params['serializer'] = 'php';
+ }
+
+ return $params;
+ }
+
+ protected function getWithToken( $key, &$casToken, $flags = 0 ) {
+ $this->debugLog( "get($key)" );
+ $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
+ $result = $this->checkResult( $key, $result );
+ return $result;
+ }
+
+ public function set( $key, $value, $exptime = 0, $flags = 0 ) {
+ $this->debugLog( "set($key)" );
+ return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
+ }
+
+ protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+ $this->debugLog( "cas($key)" );
+ return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
+ }
+
+ public function delete( $key ) {
+ $this->debugLog( "delete($key)" );
+ $result = parent::delete( $key );
+ if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
+ // "Not found" is counted as success in our interface
+ return true;
+ } else {
+ return $this->checkResult( $key, $result );
+ }
+ }
+
+ public function add( $key, $value, $exptime = 0 ) {
+ $this->debugLog( "add($key)" );
+ return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
+ }
+
+ public function incr( $key, $value = 1 ) {
+ $this->debugLog( "incr($key)" );
+ $result = $this->client->increment( $key, $value );
+ return $this->checkResult( $key, $result );
+ }
+
+ public function decr( $key, $value = 1 ) {
+ $this->debugLog( "decr($key)" );
+ $result = $this->client->decrement( $key, $value );
+ return $this->checkResult( $key, $result );
+ }
+
+ /**
+ * Check the return value from a client method call and take any necessary
+ * action. Returns the value that the wrapper function should return. At
+ * present, the return value is always the same as the return value from
+ * the client, but some day we might find a case where it should be
+ * different.
+ *
+ * @param string $key The key used by the caller, or false if there wasn't one.
+ * @param mixed $result The return value
+ * @return mixed
+ */
+ protected function checkResult( $key, $result ) {
+ if ( $result !== false ) {
+ return $result;
+ }
+ switch ( $this->client->getResultCode() ) {
+ case Memcached::RES_SUCCESS:
+ break;
+ case Memcached::RES_DATA_EXISTS:
+ case Memcached::RES_NOTSTORED:
+ case Memcached::RES_NOTFOUND:
+ $this->debugLog( "result: " . $this->client->getResultMessage() );
+ break;
+ default:
+ $msg = $this->client->getResultMessage();
+ $logCtx = [];
+ if ( $key !== false ) {
+ $server = $this->client->getServerByKey( $key );
+ $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
+ $logCtx['memcached-key'] = $key;
+ $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
+ } else {
+ $msg = "Memcached error: $msg";
+ }
+ $this->logger->error( $msg, $logCtx );
+ $this->setLastError( BagOStuff::ERR_UNEXPECTED );
+ }
+ return $result;
+ }
+
+ public function getMulti( array $keys, $flags = 0 ) {
+ $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
+ foreach ( $keys as $key ) {
+ $this->validateKeyEncoding( $key );
+ }
+ $result = $this->client->getMulti( $keys ) ?: [];
+ return $this->checkResult( false, $result );
+ }
+
+ /**
+ * @param array $data
+ * @param int $exptime
+ * @return bool
+ */
+ public function setMulti( array $data, $exptime = 0 ) {
+ $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
+ foreach ( array_keys( $data ) as $key ) {
+ $this->validateKeyEncoding( $key );
+ }
+ $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
+ return $this->checkResult( false, $result );
+ }
+
+ public function changeTTL( $key, $expiry = 0 ) {
+ $this->debugLog( "touch($key)" );
+ $result = $this->client->touch( $key, $expiry );
+ return $this->checkResult( $key, $result );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Database load monitoring.
+ *
+ * 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
+ * @ingroup Database
+ */
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * An interface for database load monitoring
+ *
+ * @ingroup Database
+ */
+interface LoadMonitor extends LoggerAwareInterface {
+ /**
+ * Construct a new LoadMonitor with a given LoadBalancer parent
+ *
+ * @param BagOStuff $sCache Server local memory cache
+ * @param BagOStuff $cCache Server local memory cache
+ * @param ILoadBalancer $parent LoadBalancer this instance serves
+ */
+ public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache );
+
+ /**
+ * Perform pre-connection load ratio adjustment.
+ * @param int[] &$loads
+ * @param string|bool $group The selected query group. Default: false
+ * @param string|bool $domain Default: false
+ */
+ public function scaleLoads( &$loads, $group = false, $domain = false );
+
+ /**
+ * Get an estimate of replication lag (in seconds) for each server
+ *
+ * Values may be "false" if replication is too broken to estimate
+ *
+ * @param integer[] $serverIndexes
+ * @param string $domain
+ *
+ * @return array Map of (server index => float|int|bool)
+ */
+ public function getLagTimes( $serverIndexes, $domain );
+
+ /**
+ * Clear any process and persistent cache of lag times
+ * @since 1.27
+ */
+ public function clearCaches();
+}
--- /dev/null
+<?php
+/**
+ * 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
+ * @ingroup Database
+ */
+
+use Psr\Log\LoggerInterface;
+
+/**
+ * Basic MySQL load monitor with no external dependencies
+ * Uses memcached to cache the replication lag for a short time
+ *
+ * @ingroup Database
+ */
+class LoadMonitorMySQL implements LoadMonitor {
+ /** @var ILoadBalancer */
+ protected $parent;
+ /** @var BagOStuff */
+ protected $srvCache;
+ /** @var BagOStuff */
+ protected $mainCache;
+ /** @var LoggerInterface */
+ protected $replLogger;
+
+ public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache ) {
+ $this->parent = $parent;
+ $this->srvCache = $sCache;
+ $this->mainCache = $cCache;
+ $this->replLogger = new \Psr\Log\NullLogger();
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->replLogger = $logger;
+ }
+
+ public function scaleLoads( &$loads, $group = false, $wiki = false ) {
+ }
+
+ public function getLagTimes( $serverIndexes, $wiki ) {
+ if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) {
+ # Single server only, just return zero without caching
+ return [ 0 => 0 ];
+ }
+
+ $key = $this->getLagTimeCacheKey();
+ # Randomize TTLs to reduce stampedes (4.0 - 5.0 sec)
+ $ttl = mt_rand( 4e6, 5e6 ) / 1e6;
+ # Keep keys around longer as fallbacks
+ $staleTTL = 60;
+
+ # (a) Check the local APC cache
+ $value = $this->srvCache->get( $key );
+ if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+ $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from local cache" );
+ return $value['lagTimes']; // cache hit
+ }
+ $staleValue = $value ?: false;
+
+ # (b) Check the shared cache and backfill APC
+ $value = $this->mainCache->get( $key );
+ if ( $value && $value['timestamp'] > ( microtime( true ) - $ttl ) ) {
+ $this->srvCache->set( $key, $value, $staleTTL );
+ $this->replLogger->debug( __METHOD__ . ": got lag times ($key) from main cache" );
+
+ return $value['lagTimes']; // cache hit
+ }
+ $staleValue = $value ?: $staleValue;
+
+ # (c) Cache key missing or expired; regenerate and backfill
+ if ( $this->mainCache->lock( $key, 0, 10 ) ) {
+ # Let this process alone update the cache value
+ $cache = $this->mainCache;
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $unlocker = new ScopedCallback( function () use ( $cache, $key ) {
+ $cache->unlock( $key );
+ } );
+ } elseif ( $staleValue ) {
+ # Could not acquire lock but an old cache exists, so use it
+ return $staleValue['lagTimes'];
+ }
+
+ $lagTimes = [];
+ foreach ( $serverIndexes as $i ) {
+ if ( $i == $this->parent->getWriterIndex() ) {
+ $lagTimes[$i] = 0; // master always has no lag
+ continue;
+ }
+
+ $conn = $this->parent->getAnyOpenConnection( $i );
+ if ( $conn ) {
+ $close = false; // already open
+ } else {
+ $conn = $this->parent->openConnection( $i, $wiki );
+ $close = true; // new connection
+ }
+
+ if ( !$conn ) {
+ $lagTimes[$i] = false;
+ $host = $this->parent->getServerName( $i );
+ $this->replLogger->error( __METHOD__ . ": host $host (#$i) is unreachable" );
+ continue;
+ }
+
+ $lagTimes[$i] = $conn->getLag();
+ if ( $lagTimes[$i] === false ) {
+ $host = $this->parent->getServerName( $i );
+ $this->replLogger->error( __METHOD__ . ": host $host (#$i) is not replicating?" );
+ }
+
+ if ( $close ) {
+ # Close the connection to avoid sleeper connections piling up.
+ # Note that the caller will pick one of these DBs and reconnect,
+ # which is slightly inefficient, but this only matters for the lag
+ # time cache miss cache, which is far less common that cache hits.
+ $this->parent->closeConnection( $conn );
+ }
+ }
+
+ # Add a timestamp key so we know when it was cached
+ $value = [ 'lagTimes' => $lagTimes, 'timestamp' => microtime( true ) ];
+ $this->mainCache->set( $key, $value, $staleTTL );
+ $this->srvCache->set( $key, $value, $staleTTL );
+ $this->replLogger->info( __METHOD__ . ": re-calculated lag times ($key)" );
+
+ return $value['lagTimes'];
+ }
+
+ public function clearCaches() {
+ $key = $this->getLagTimeCacheKey();
+ $this->srvCache->delete( $key );
+ $this->mainCache->delete( $key );
+ }
+
+ private function getLagTimeCacheKey() {
+ $writerIndex = $this->parent->getWriterIndex();
+ // Lag is per-server, not per-DB, so key on the master DB name
+ return $this->srvCache->makeGlobalKey(
+ 'lag-times',
+ $this->parent->getServerName( $writerIndex )
+ );
+ }
+}
--- /dev/null
+<?php
+/**
+ * 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
+ * @ingroup Database
+ */
+use Psr\Log\LoggerInterface;
+
+class LoadMonitorNull implements LoadMonitor {
+ public function __construct( ILoadBalancer $parent, BagOStuff $sCache, BagOStuff $cCache ) {
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ }
+
+ public function scaleLoads( &$loads, $group = false, $wiki = false ) {
+ }
+
+ public function getLagTimes( $serverIndexes, $wiki ) {
+ return array_fill_keys( $serverIndexes, 0 );
+ }
+
+ public function clearCaches() {
+
+ }
+}
+++ /dev/null
-<?php
-/**
- * Object caching using memcached.
- *
- * 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
- * @ingroup Cache
- */
-
-/**
- * A wrapper class for the PECL memcached client
- *
- * @ingroup Cache
- */
-class MemcachedPeclBagOStuff extends MemcachedBagOStuff {
-
- /**
- * Constructor
- *
- * Available parameters are:
- * - servers: The list of IP:port combinations holding the memcached servers.
- * - persistent: Whether to use a persistent connection
- * - compress_threshold: The minimum size an object must be before it is compressed
- * - timeout: The read timeout in microseconds
- * - connect_timeout: The connect timeout in seconds
- * - retry_timeout: Time in seconds to wait before retrying a failed connect attempt
- * - server_failure_limit: Limit for server connect failures before it is removed
- * - serializer: May be either "php" or "igbinary". Igbinary produces more compact
- * values, but serialization is much slower unless the php.ini option
- * igbinary.compact_strings is off.
- * - use_binary_protocol Whether to enable the binary protocol (default is ASCII) (boolean)
- * @param array $params
- * @throws InvalidArgumentException
- */
- function __construct( $params ) {
- parent::__construct( $params );
- $params = $this->applyDefaultParams( $params );
-
- if ( $params['persistent'] ) {
- // The pool ID must be unique to the server/option combination.
- // The Memcached object is essentially shared for each pool ID.
- // We can only reuse a pool ID if we keep the config consistent.
- $this->client = new Memcached( md5( serialize( $params ) ) );
- if ( count( $this->client->getServerList() ) ) {
- $this->logger->debug( __METHOD__ . ": persistent Memcached object already loaded." );
- return; // already initialized; don't add duplicate servers
- }
- } else {
- $this->client = new Memcached;
- }
-
- if ( $params['use_binary_protocol'] ) {
- $this->client->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
- }
-
- if ( isset( $params['retry_timeout'] ) ) {
- $this->client->setOption( Memcached::OPT_RETRY_TIMEOUT, $params['retry_timeout'] );
- }
-
- if ( isset( $params['server_failure_limit'] ) ) {
- $this->client->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, $params['server_failure_limit'] );
- }
-
- // The compression threshold is an undocumented php.ini option for some
- // reason. There's probably not much harm in setting it globally, for
- // compatibility with the settings for the PHP client.
- ini_set( 'memcached.compression_threshold', $params['compress_threshold'] );
-
- // Set timeouts
- $this->client->setOption( Memcached::OPT_CONNECT_TIMEOUT, $params['connect_timeout'] * 1000 );
- $this->client->setOption( Memcached::OPT_SEND_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_RECV_TIMEOUT, $params['timeout'] );
- $this->client->setOption( Memcached::OPT_POLL_TIMEOUT, $params['timeout'] / 1000 );
-
- // Set libketama mode since it's recommended by the documentation and
- // is as good as any. There's no way to configure libmemcached to use
- // hashes identical to the ones currently in use by the PHP client, and
- // even implementing one of the libmemcached hashes in pure PHP for
- // forwards compatibility would require MemcachedClient::get_sock() to be
- // rewritten.
- $this->client->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
-
- // Set the serializer
- switch ( $params['serializer'] ) {
- case 'php':
- $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_PHP );
- break;
- case 'igbinary':
- if ( !Memcached::HAVE_IGBINARY ) {
- throw new InvalidArgumentException(
- __CLASS__ . ': the igbinary extension is not available ' .
- 'but igbinary serialization was requested.'
- );
- }
- $this->client->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
- break;
- default:
- throw new InvalidArgumentException(
- __CLASS__ . ': invalid value for serializer parameter'
- );
- }
- $servers = [];
- foreach ( $params['servers'] as $host ) {
- $servers[] = IP::splitHostAndPort( $host ); // (ip, port)
- }
- $this->client->addServers( $servers );
- }
-
- protected function applyDefaultParams( $params ) {
- $params = parent::applyDefaultParams( $params );
-
- if ( !isset( $params['use_binary_protocol'] ) ) {
- $params['use_binary_protocol'] = false;
- }
-
- if ( !isset( $params['serializer'] ) ) {
- $params['serializer'] = 'php';
- }
-
- return $params;
- }
-
- protected function getWithToken( $key, &$casToken, $flags = 0 ) {
- $this->debugLog( "get($key)" );
- $result = $this->client->get( $this->validateKeyEncoding( $key ), null, $casToken );
- $result = $this->checkResult( $key, $result );
- return $result;
- }
-
- public function set( $key, $value, $exptime = 0, $flags = 0 ) {
- $this->debugLog( "set($key)" );
- return $this->checkResult( $key, parent::set( $key, $value, $exptime ) );
- }
-
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
- $this->debugLog( "cas($key)" );
- return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
- }
-
- public function delete( $key ) {
- $this->debugLog( "delete($key)" );
- $result = parent::delete( $key );
- if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTFOUND ) {
- // "Not found" is counted as success in our interface
- return true;
- } else {
- return $this->checkResult( $key, $result );
- }
- }
-
- public function add( $key, $value, $exptime = 0 ) {
- $this->debugLog( "add($key)" );
- return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
- }
-
- public function incr( $key, $value = 1 ) {
- $this->debugLog( "incr($key)" );
- $result = $this->client->increment( $key, $value );
- return $this->checkResult( $key, $result );
- }
-
- public function decr( $key, $value = 1 ) {
- $this->debugLog( "decr($key)" );
- $result = $this->client->decrement( $key, $value );
- return $this->checkResult( $key, $result );
- }
-
- /**
- * Check the return value from a client method call and take any necessary
- * action. Returns the value that the wrapper function should return. At
- * present, the return value is always the same as the return value from
- * the client, but some day we might find a case where it should be
- * different.
- *
- * @param string $key The key used by the caller, or false if there wasn't one.
- * @param mixed $result The return value
- * @return mixed
- */
- protected function checkResult( $key, $result ) {
- if ( $result !== false ) {
- return $result;
- }
- switch ( $this->client->getResultCode() ) {
- case Memcached::RES_SUCCESS:
- break;
- case Memcached::RES_DATA_EXISTS:
- case Memcached::RES_NOTSTORED:
- case Memcached::RES_NOTFOUND:
- $this->debugLog( "result: " . $this->client->getResultMessage() );
- break;
- default:
- $msg = $this->client->getResultMessage();
- $logCtx = [];
- if ( $key !== false ) {
- $server = $this->client->getServerByKey( $key );
- $logCtx['memcached-server'] = "{$server['host']}:{$server['port']}";
- $logCtx['memcached-key'] = $key;
- $msg = "Memcached error for key \"{memcached-key}\" on server \"{memcached-server}\": $msg";
- } else {
- $msg = "Memcached error: $msg";
- }
- $this->logger->error( $msg, $logCtx );
- $this->setLastError( BagOStuff::ERR_UNEXPECTED );
- }
- return $result;
- }
-
- public function getMulti( array $keys, $flags = 0 ) {
- $this->debugLog( 'getMulti(' . implode( ', ', $keys ) . ')' );
- foreach ( $keys as $key ) {
- $this->validateKeyEncoding( $key );
- }
- $result = $this->client->getMulti( $keys ) ?: [];
- return $this->checkResult( false, $result );
- }
-
- /**
- * @param array $data
- * @param int $exptime
- * @return bool
- */
- public function setMulti( array $data, $exptime = 0 ) {
- $this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
- foreach ( array_keys( $data ) as $key ) {
- $this->validateKeyEncoding( $key );
- }
- $result = $this->client->setMulti( $data, $this->fixExpiry( $exptime ) );
- return $this->checkResult( false, $result );
- }
-
- public function changeTTL( $key, $expiry = 0 ) {
- $this->debugLog( "touch($key)" );
- $result = $this->client->touch( $key, $expiry );
- return $this->checkResult( $key, $result );
- }
-}
/**
* Set and return the mOffset timestamp such that we can get all revisions with
* a timestamp up to the specified parameters.
- * @param int $year [optional] Year up to which we want revisions. Default is current year.
- * @param int $month [optional] Month up to which we want revisions. Default is end of year.
+ * @param int $year Year up to which we want revisions
+ * @param int $month Month up to which we want revisions
* @param int $day [optional] Day up to which we want revisions. Default is end of month.
- * @return string Timestamp
+ * @return string|null Timestamp or null if year and month are false/invalid
*/
- function getDateCond( $year = -1, $month = -1, $day = -1 ) {
+ function getDateCond( $year, $month, $day = -1 ) {
$year = intval( $year );
$month = intval( $month );
$day = intval( $day );
$this->mYear = $year > 0 ? $year : false;
$this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false;
+ // If year and month are false, don't update the mOffset
+ if ( !$this->mYear && !$this->mMonth ) {
+ return;
+ }
+
// Given an optional year, month, and day, we need to generate a timestamp
// to use as "WHERE rev_timestamp <= result"
// Examples: year = 2006 equals < 20070101 (+000000)
# Truncate for whole multibyte characters.
$reason = $wgContLang->truncate( $reason, 255 );
+ // Run edit filters
+ $derivativeContext = new DerivativeContext( $this->getContext() );
+ $derivativeContext->setTitle( $this->title );
+ $derivativeContext->setWikiPage( $page );
+ $status = new Status();
+ if ( !Hooks::run( 'EditFilterMergedContent',
+ [ $derivativeContext, $newContent, $status, $reason,
+ $user, false ] )
+ ) {
+ if ( $status->isGood() ) {
+ // TODO: extensions should really specify an error message
+ $status->fatal( 'hookaborted' );
+ }
+ return $status;
+ }
+
$status = $page->doEditContent(
$newContent,
$reason,
*/
public static function canonicalizeLoginData( $username, $password ) {
$sep = BotPassword::getSeparator();
- if ( strpos( $username, $sep ) !== false ) {
- // the separator is not valid in usernames so this must be a bot login
- return [ $username, $password, false ];
+ // the strlen check helps minimize the password information obtainable from timing
+ if ( strlen( $password ) >= 32 && strpos( $username, $sep ) !== false ) {
+ // the separator is not valid in new usernames but might appear in legacy ones
+ if ( preg_match( '/^[0-9a-w]{32,}$/', $password ) ) {
+ return [ $username, $password, true ];
+ }
} elseif ( strlen( $password ) > 32 && strpos( $password, $sep ) !== false ) {
- // the strlen check helps minimize the password information obtainable from timing
$segments = explode( $sep, $password );
$password = array_pop( $segments );
$appId = implode( $sep, $segments );
* @ingroup Maintenance
*/
class RefreshLinks extends Maintenance {
+ /** @var int|bool */
+ protected $namespace = false;
+
public function __construct() {
parent::__construct();
$this->addDescription( 'Refresh link tables' );
$this->addOption( 'e', 'Last page id to refresh', false, true );
$this->addOption( 'dfn-chunk-size', 'Maximum number of existent IDs to check per ' .
'query, default 100000', false, true );
+ $this->addOption( 'namespace', 'Only fix pages in this namespace', false, true );
$this->addArg( 'start', 'Page_id to start from, default 1', false );
$this->setBatchSize( 100 );
}
$start = (int)$this->getArg( 0 ) ?: null;
$end = (int)$this->getOption( 'e' ) ?: null;
$dfnChunkSize = (int)$this->getOption( 'dfn-chunk-size', 100000 );
+ $ns = $this->getOption( 'namespace' );
+ if ( $ns === null ) {
+ $this->namespace = false;
+ } else {
+ $this->namespace = (int)$ns;
+ }
if ( !$this->hasOption( 'dfn-only' ) ) {
$new = $this->getOption( 'new-only', false );
$redir = $this->getOption( 'redirects-only', false );
}
}
+ private function namespaceCond() {
+ return $this->namespace !== false
+ ? [ 'page_namespace' => $this->namespace ]
+ : [];
+ }
+
/**
* Do the actual link refreshing.
* @param int|null $start Page_id to start from
"page_is_redirect=1",
"rd_from IS NULL",
self::intervalCond( $dbr, 'page_id', $start, $end ),
- ];
+ ] + $this->namespaceCond();
$res = $dbr->select(
[ 'page', 'redirect' ],
[
'page_is_new' => 1,
self::intervalCond( $dbr, 'page_id', $start, $end ),
- ],
+ ] + $this->namespaceCond(),
__METHOD__
);
$num = $res->numRows();
if ( $redirectsOnly ) {
$this->fixRedirect( $row->page_id );
} else {
- self::fixLinksFromArticle( $row->page_id );
+ self::fixLinksFromArticle( $row->page_id, $this->namespace );
}
}
} else {
$this->output( "$id\n" );
wfWaitForSlaves();
}
- self::fixLinksFromArticle( $id );
+ self::fixLinksFromArticle( $id, $this->namespace );
}
}
}
$dbw->delete( 'redirect', [ 'rd_from' => $id ],
__METHOD__ );
+ return;
+ } elseif ( $this->namespace !== false
+ && !$page->getTitle()->inNamespace( $this->namespace )
+ ) {
return;
}
/**
* Run LinksUpdate for all links on a given page_id
* @param int $id The page_id
+ * @param int|bool $ns Only fix links if it is in this namespace
*/
- public static function fixLinksFromArticle( $id ) {
+ public static function fixLinksFromArticle( $id, $ns = false ) {
$page = WikiPage::newFromID( $id );
LinkCache::singleton()->clear();
if ( $page === null ) {
return;
+ } elseif ( $ns !== false
+ && !$page->getTitle()->inNamespace( $ns ) ) {
+ return;
}
$content = $page->getContent( Revision::RAW );
$nextStart = $dbr->selectField(
'page',
'page_id',
- self::intervalCond( $dbr, 'page_id', $start, $end ),
+ [ self::intervalCond( $dbr, 'page_id', $start, $end ) ]
+ + $this->namespaceCond(),
__METHOD__,
[ 'ORDER BY' => 'page_id', 'OFFSET' => $chunkSize ]
);
throw new MWException( "duplicate article '$name' at $file:$line\n" );
}
- $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
+ $status = $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
+ if ( !$status->isOK() ) {
+ throw new MWException( $status->getWikiText( false, false, 'en' ) );
+ }
// The RepoGroup cache is invalidated by the creation of file redirects
if ( $title->getNamespace() === NS_IMAGE ) {
$centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
$this->assertNotEquals( 0, $centralId, 'sanity check' );
+ $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
$passwordFactory = new PasswordFactory();
$passwordFactory->init( RequestContext::getMain()->getConfig() );
// A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
- $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
+ $passwordHash = $passwordFactory->newFromPlaintext( $password );
$dbw = wfGetDB( DB_MASTER );
$dbw->insert(
$ret = $this->doApiRequest( [
'action' => 'login',
'lgname' => $lgName,
- 'lgpassword' => 'foobaz',
+ 'lgpassword' => $password,
] );
$result = $ret[0];
'action' => 'login',
'lgtoken' => $token,
'lgname' => $lgName,
- 'lgpassword' => 'foobaz',
+ 'lgpassword' => $password,
], $ret[2] );
$result = $ret[0];
$currYear = $timestamp->format( 'Y' );
$currMonth = $timestamp->format( 'n' );
- $currYearTimestamp = $db->timestamp( $currYear + 1 . '0101000000' );
// Test that getDateCond sets and returns mOffset
- $this->assertEquals( $pager->getDateCond( 2006 ), $pager->mOffset );
-
- // Test year
- $pager->getDateCond( 2006 );
- $this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
+ $this->assertEquals( $pager->getDateCond( 2006, 6 ), $pager->mOffset );
// Test year and month
$pager->getDateCond( 2006, 6 );
$pager->getDateCond( 2006, 6, 30 );
$this->assertEquals( $pager->mOffset, $db->timestamp( '20060701000000' ) );
- // Test invalid year (should use current year)
- $pager->getDateCond( -1337 );
- $this->assertEquals( $pager->mOffset, $currYearTimestamp );
-
- // Test invalid month
+ // Test invalid month (should use end of year)
$pager->getDateCond( 2006, -1 );
$this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
- // Test invalid day
+ // Test invalid day (should use end of month)
$pager->getDateCond( 2006, 6, 1337 );
$this->assertEquals( $pager->mOffset, $db->timestamp( '20060701000000' ) );
- // Test no year or month (should use end of current year)
- $pager->getDateCond();
- $this->assertEquals( $pager->mOffset, $currYearTimestamp );
-
// Test last day of year
$pager->getDateCond( 2006, 12, 31 );
$this->assertEquals( $pager->mOffset, $db->timestamp( '20070101000000' ) );
return [
[ 'user', 'pass', false ],
[ 'user', 'abc@def', false ],
+ [ 'legacy@user', 'pass', false ],
[ 'user@bot', '12345678901234567890123456789012',
- [ 'user@bot', '12345678901234567890123456789012', false ] ],
+ [ 'user@bot', '12345678901234567890123456789012', true ] ],
[ 'user', 'bot@12345678901234567890123456789012',
[ 'user@bot', '12345678901234567890123456789012', true ] ],
[ 'user', 'bot@12345678901234567890123456789012345',