rdbms: various cleanups to LoadBalancer::reallyOpenConnection()

Move the DBO_TRX init logic out of Database::__construct() and into
LoadBalancer since the later already handles setting and clearing this
flag based on transaction rounds starting and ending.

Add 'lazyMasterHandle', 'topologyRole', and 'topologicalMaster' parameters
to Database::factory() and inject them via LoadBalancer all at once in order
to avoid worrying about call order. Move some type casting code to
Database::__construct().

Add IDatabase::getTopologyRole()/getTopologicalMaster().

Use constants for getLBInfo()/setLBInfo() for better usage tracking and
typo resistance.

Change-Id: I437ce434326601e6ba36d9aedc55db396dfe4452
This commit is contained in:
Aaron Schulz 2019-07-14 15:43:26 -07:00
parent cd7972620f
commit fb621c26a3
15 changed files with 408 additions and 286 deletions

View file

@ -80,6 +80,14 @@ class DBConnRef implements IDatabase {
return $this->__call( __FUNCTION__, func_get_args() );
}
public function getTopologyRole() {
return $this->__call( __FUNCTION__, func_get_args() );
}
public function getTopologyRootMaster() {
return $this->__call( __FUNCTION__, func_get_args() );
}
/**
* @param bool|null $buffer
* @return bool
@ -140,11 +148,6 @@ class DBConnRef implements IDatabase {
throw new DBUnexpectedError( $this, "Changing LB info is disallowed to enable reuse." );
}
public function setLazyMasterHandle( IDatabase $conn ) {
// Disallow things that might confuse the LoadBalancer tracking
throw new DBUnexpectedError( $this, "Database injection is disallowed to enable reuse." );
}
public function implicitOrderby() {
return $this->__call( __FUNCTION__, func_get_args() );
}

View file

@ -81,6 +81,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
protected $cliMode;
/** @var string Agent name for query profiling */
protected $agent;
/** @var string Replication topology role of the server; one of the class ROLE_* constants */
protected $topologyRole;
/** @var string|null Host (or address) of the root master server for the replication topology */
protected $topologyRootMaster;
/** @var array Parameters used by initConnection() to establish a connection */
protected $connectionParams;
/** @var string[]|int[]|float[] SQL variables values to use for all new connections */
@ -180,6 +184,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/** @var int|null Integer ID of the managing LBFactory instance or null if none */
private $ownerId;
/** @var string Whether the database is a file on disk */
const ATTR_DB_IS_FILE = 'db-is-file';
/** @var string Lock granularity is on the level of the entire database */
const ATTR_DB_LEVEL_LOCKING = 'db-level-locking';
/** @var string The SCHEMA keyword refers to a grouping of tables in a database */
@ -243,24 +249,27 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* @param array $params Parameters passed from Database::factory()
*/
public function __construct( array $params ) {
$this->connectionParams = [];
foreach ( [ 'host', 'user', 'password', 'dbname', 'schema', 'tablePrefix' ] as $name ) {
$this->connectionParams[$name] = $params[$name];
}
$this->connectionParams = [
'host' => strlen( $params['host'] ) ? $params['host'] : null,
'user' => strlen( $params['user'] ) ? $params['user'] : null,
'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
'password' => is_string( $params['password'] ) ? $params['password'] : null,
'tablePrefix' => (string)$params['tablePrefix']
];
$this->lbInfo = $params['lbInfo'] ?? [];
$this->lazyMasterHandle = $params['lazyMasterHandle'] ?? null;
$this->connectionVariables = $params['variables'] ?? [];
$this->cliMode = $params['cliMode'];
$this->agent = $params['agent'];
$this->flags = $params['flags'];
if ( $this->flags & self::DBO_DEFAULT ) {
if ( $this->cliMode ) {
$this->flags &= ~self::DBO_TRX;
} else {
$this->flags |= self::DBO_TRX;
}
}
$this->flags = (int)$params['flags'];
$this->cliMode = (bool)$params['cliMode'];
$this->agent = (string)$params['agent'];
$this->topologyRole = (string)$params['topologyRole'];
$this->topologyRootMaster = (string)$params['topologicalMaster'];
$this->nonNativeInsertSelectBatchSize = $params['nonNativeInsertSelectBatchSize'] ?? 10000;
$this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
$this->srvCache = $params['srvCache'];
$this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = $params['trxProfiler'];
$this->connLogger = $params['connLogger'];
@ -351,6 +360,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
* 'mysqli' driver; the old one 'mysql' has been removed.
* - variables: Optional map of session variables to set after connecting. This can be
* used to adjust lock timeouts or encoding modes and the like.
* - topologyRole: Optional IDatabase::ROLE_* constant for the server.
* - topologicalMaster: Optional name of the master server within the replication topology.
* - lbInfo: Optional map of field/values for the managing load balancer instance.
* The "master" and "replica" fields are used to flag the replication role of this
* database server and whether methods like getLag() should actually issue queries.
* - lazyMasterHandle: lazy-connecting IDatabase handle to the master DB for the cluster
* that this database belongs to. This is used for replication status purposes.
* - connLogger: Optional PSR-3 logger interface instance.
* - queryLogger: Optional PSR-3 logger interface instance.
* - profiler : Optional callback that takes a section name argument and returns
@ -375,6 +391,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
$params += [
// Default configuration
'host' => null,
'user' => null,
'password' => null,
@ -383,24 +400,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
'tablePrefix' => '',
'flags' => 0,
'variables' => [],
'lbInfo' => [],
'cliMode' => ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ),
'agent' => basename( $_SERVER['SCRIPT_NAME'] ) . '@' . gethostname(),
'ownerId' => null
];
$normalizedParams = [
// Configuration
'host' => strlen( $params['host'] ) ? $params['host'] : null,
'user' => strlen( $params['user'] ) ? $params['user'] : null,
'password' => is_string( $params['password'] ) ? $params['password'] : null,
'dbname' => strlen( $params['dbname'] ) ? $params['dbname'] : null,
'schema' => strlen( $params['schema'] ) ? $params['schema'] : null,
'tablePrefix' => (string)$params['tablePrefix'],
'flags' => (int)$params['flags'],
'variables' => $params['variables'],
'cliMode' => (bool)$params['cliMode'],
'agent' => (string)$params['agent'],
'ownerId' => null,
'topologyRole' => null,
'topologicalMaster' => null,
// Objects and callbacks
'lazyMasterHandle' => $params['lazyMasterHandle'] ?? null,
'srvCache' => $params['srvCache'] ?? new HashBagOStuff(),
'profiler' => $params['profiler'] ?? null,
'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(),
@ -412,10 +419,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
'deprecationLogger' => $params['deprecationLogger'] ?? function ( $msg ) {
trigger_error( $msg, E_USER_DEPRECATED );
}
] + $params;
];
/** @var Database $conn */
$conn = new $class( $normalizedParams );
$conn = new $class( $params );
if ( $connect === self::NEW_CONNECTED ) {
$conn->initConnection();
}
@ -435,6 +442,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
*/
final public static function attributesFromType( $dbType, $driver = null ) {
static $defaults = [
self::ATTR_DB_IS_FILE => false,
self::ATTR_DB_LEVEL_LOCKING => false,
self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
];
@ -520,6 +528,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
return $this->getServerVersion();
}
public function getTopologyRole() {
return $this->topologyRole;
}
public function getTopologyRootMaster() {
return $this->topologyRootMaster;
}
/**
* Backwards-compatibility no-op method for disabling query buffering
*
@ -611,13 +627,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
public function setLazyMasterHandle( IDatabase $conn ) {
$this->lazyMasterHandle = $conn;
}
/**
* Get a handle to the master server of the cluster to which this server belongs
*
* @return IDatabase|null
* @see setLazyMasterHandle()
* @since 1.27
*/
protected function getLazyMasterHandle() {
@ -660,7 +673,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
final protected function getTransactionRoundId() {
// If transaction round participation is enabled, see if one is active
if ( $this->getFlag( self::DBO_TRX ) ) {
$id = $this->getLBInfo( 'trxRoundId' );
$id = $this->getLBInfo( self::LB_TRX_ROUND_ID );
return is_string( $id ) ? $id : null;
}
@ -963,19 +976,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
/**
* Make sure that this server is not marked as a replica nor read-only as a sanity check
*
* @throws DBReadOnlyRoleError
* @throws DBReadOnlyError
*/
protected function assertIsWritableMaster() {
if ( $this->getLBInfo( 'replica' ) ) {
throw new DBReadOnlyRoleError(
$this,
'Write operations are not allowed on replica database connections'
);
}
$reason = $this->getReadOnlyReason();
if ( $reason !== false ) {
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
$info = $this->getReadOnlyReason();
if ( $info ) {
list( $reason, $source ) = $info;
if ( $source === 'role' ) {
throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
} else {
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
}
}
@ -1285,7 +1296,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
}
$prefix = $this->getLBInfo( 'master' ) ? 'query-m: ' : 'query: ';
$prefix = $this->topologyRole ? 'query-m: ' : 'query: ';
$generalizedSql = new GeneralizedSql( $sql, $this->trxShortId, $prefix );
$startTime = microtime( true );
@ -4384,14 +4395,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
* Get a replica DB lag estimate for this server
* Get a replica DB lag estimate for this server at the start of a transaction
*
* This is a no-op unless the server is known a priori to be a replica DB
*
* @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
* @since 1.27
*/
protected function getApproximateLagStatus() {
return [
'lag' => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
'lag' => ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) ? $this->getLag() : 0,
'since' => microtime( true )
];
}
@ -4433,9 +4446,9 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
public function getLag() {
if ( $this->getLBInfo( 'master' ) ) {
if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
return 0; // this is the master
} elseif ( $this->getLBInfo( 'is static' ) ) {
} elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
return 0; // static dataset
}
@ -4814,14 +4827,18 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
}
/**
* @return string|bool Reason this DB is read-only or false if it is not
* @return array|bool Tuple of (read-only reason, "role" or "lb") or false if it is not
*/
protected function getReadOnlyReason() {
$reason = $this->getLBInfo( 'readOnlyReason' );
if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
return [ 'Server is configured as a read-only replica database.', 'role' ];
} elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
return [ 'Server is configured as a read-only static clone database.', 'role' ];
}
$reason = $this->getLBInfo( self::LB_READ_ONLY_REASON );
if ( is_string( $reason ) ) {
return $reason;
} elseif ( $this->getLBInfo( 'replica' ) ) {
return "Server is configured in the role of a read-only replica database.";
return [ $reason, 'lb' ];
}
return false;

View file

@ -770,7 +770,7 @@ abstract class DatabaseMysqlBase extends Database {
'mysql',
'master-info',
// Using one key for all cluster replica DBs is preferable
$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
$this->topologyRootMaster ?? $this->getServer()
);
$fname = __METHOD__;
@ -849,7 +849,7 @@ abstract class DatabaseMysqlBase extends Database {
throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
}
if ( $this->getLBInfo( 'is static' ) === true ) {
if ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
$this->queryLogger->debug(
"Bypassed replication wait; database has a static dataset",
$this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )

View file

@ -95,8 +95,8 @@ class DatabasePostgres extends Database {
$this->password = $password;
$connectVars = [
// pg_connect() user $user as the default database. Since a database is required,
// then pick a "don't care" database that is more likely to exist than that one.
// A database must be specified in order to connect to Postgres. If $dbName is not
// specified, then use the standard "postgres" database that should exist by default.
'dbname' => strlen( $dbName ) ? $dbName : 'postgres',
'user' => $user,
'password' => $password
@ -1442,7 +1442,7 @@ SQL;
return $row ? ( strtolower( $row->default_transaction_read_only ) === 'on' ) : false;
}
public static function getAttributes() {
protected static function getAttributes() {
return [ self::ATTR_SCHEMAS_AS_TABLE_GROUPS => true ];
}

View file

@ -63,21 +63,21 @@ class DatabaseSqlite extends Database {
* - dbDirectory : directory containing the DB and the lock file directory
* - dbFilePath : use this to force the path of the DB file
* - trxMode : one of (deferred, immediate, exclusive)
* @param array $p
* @param array $params
*/
public function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
$this->dbPath = $p['dbFilePath'];
if ( !strlen( $p['dbname'] ) ) {
$p['dbname'] = self::generateDatabaseName( $this->dbPath );
public function __construct( array $params ) {
if ( isset( $params['dbFilePath'] ) ) {
$this->dbPath = $params['dbFilePath'];
if ( !strlen( $params['dbname'] ) ) {
$params['dbname'] = self::generateDatabaseName( $this->dbPath );
}
} elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
} elseif ( isset( $params['dbDirectory'] ) ) {
$this->dbDir = $params['dbDirectory'];
}
parent::__construct( $p );
parent::__construct( $params );
$this->trxMode = strtoupper( $p['trxMode'] ?? '' );
$this->trxMode = strtoupper( $params['trxMode'] ?? '' );
$lockDirectory = $this->getLockFileDirectory();
if ( $lockDirectory !== null ) {
@ -91,7 +91,10 @@ class DatabaseSqlite extends Database {
}
protected static function getAttributes() {
return [ self::ATTR_DB_LEVEL_LOCKING => true ];
return [
self::ATTR_DB_IS_FILE => true,
self::ATTR_DB_LEVEL_LOCKING => true
];
}
/**
@ -148,6 +151,8 @@ class DatabaseSqlite extends Database {
throw $this->newExceptionAfterConnectError( "Got mode '{$this->trxMode}' for BEGIN" );
}
$this->server = 'localhost';
$attributes = [];
if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
// Persistent connections can avoid some schema index reading overhead.

View file

@ -95,7 +95,7 @@ interface IDatabase {
const DBO_IGNORE = 4;
/** @var int Automatically start a transaction before running a query if none is active */
const DBO_TRX = 8;
/** @var int Use DBO_TRX in non-CLI mode */
/** @var int Join load balancer transaction rounds (which control DBO_TRX) in non-CLI mode */
const DBO_DEFAULT = 16;
/** @var int Use DB persistent connections if possible */
const DBO_PERSISTENT = 32;
@ -129,6 +129,20 @@ interface IDatabase {
/** @var bool Parameter to unionQueries() for UNION DISTINCT */
const UNION_DISTINCT = false;
/** @var string Field for getLBInfo()/setLBInfo() */
const LB_TRX_ROUND_ID = 'trxRoundId';
/** @var string Field for getLBInfo()/setLBInfo() */
const LB_READ_ONLY_REASON = 'readOnlyReason';
/** @var string Master server than can stream OLTP updates to replica servers */
const ROLE_STREAMING_MASTER = 'streaming-master';
/** @var string Replica server that streams OLTP updates from the master server */
const ROLE_STREAMING_REPLICA = 'streaming-replica';
/** @var string Replica server of a static dataset that does not get OLTP updates */
const ROLE_STATIC_CLONE = 'static-clone';
/** @var string Unknown replication topology role */
const ROLE_UNKNOWN = 'unknown';
/**
* Get a human-readable string describing the current software version
*
@ -138,6 +152,22 @@ interface IDatabase {
*/
public function getServerInfo();
/**
* Get the replication topology role of this server
*
* @return string One of the class ROLE_* constants
* @since 1.34
*/
public function getTopologyRole();
/**
* Get the host (or address) of the root master server for the replication topology
*
* @return string|null Master server name or null if not known
* @since 1.34
*/
public function getTopologyRootMaster();
/**
* Gets the current transaction level.
*
@ -202,19 +232,13 @@ interface IDatabase {
/**
* Set the entire array or a particular key of the managing load balancer info array
*
* Keys matching the IDatabase::LB_* constants are also used internally by subclasses
*
* @param array|string $nameOrArray The new array or the name of a key to set
* @param array|null $value If $nameOrArray is a string, the new key value (null to unset)
*/
public function setLBInfo( $nameOrArray, $value = null );
/**
* Set a lazy-connecting DB handle to the master DB (for replication status purposes)
*
* @param IDatabase $conn
* @since 1.27
*/
public function setLazyMasterHandle( IDatabase $conn );
/**
* Returns true if this database does an implicit order by when the column has an index
* For example: SELECT page_title FROM page LIMIT 1
@ -1088,8 +1112,9 @@ interface IDatabase {
*
* In systems like mysql/mariadb, different databases can easily be referenced on a single
* connection merely by name, even in a single query via JOIN. On the other hand, Postgres
* treats databases as fully separate, only allowing mechanisms like postgres_fdw to
* effectively "mount" foreign DBs. This is true even among DBs on the same server.
* treats databases as logically separate, with different database users, requiring special
* mechanisms like postgres_fdw to "mount" foreign DBs. This is true even among DBs on the
* same server. Changing the selected database via selectDomain() requires a new connection.
*
* @return bool
* @since 1.29
@ -2048,11 +2073,11 @@ interface IDatabase {
public function setSessionOptions( array $options );
/**
* Set variables to be used in sourceFile/sourceStream, in preference to the
* ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at
* all. If it's set to false, $GLOBALS will be used.
* Set schema variables to be used when streaming commands from SQL files or stdin
*
* @param bool|array $vars Mapping variable name to value.
* Variables appear as SQL comments and are substituted by their corresponding values
*
* @param array|null $vars Map of (variable => value) or null to use the defaults
*/
public function setSchemaVars( $vars );

View file

@ -58,20 +58,11 @@ class LBFactorySimple extends LBFactory {
parent::__construct( $conf );
$this->mainServers = $conf['servers'] ?? [];
foreach ( $this->mainServers as $i => $server ) {
if ( $i == 0 ) {
$this->mainServers[$i]['master'] = true;
} else {
$this->mainServers[$i]['replica'] = true;
}
}
foreach ( ( $conf['externalClusters'] ?? [] ) as $cluster => $servers ) {
foreach ( $servers as $index => $server ) {
$this->externalServersByCluster[$cluster][$index] = $server;
}
}
$this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
}

View file

@ -77,8 +77,8 @@ class LoadBalancer implements ILoadBalancer {
private $servers;
/** @var array[] Map of (group => server index => weight) */
private $groupLoads;
/** @var bool Whether to disregard replica DB lag as a factor in replica DB selection */
private $allowLagged;
/** @var int[] Map of (server index => seconds of lag considered "high") */
private $maxLagByIndex;
/** @var int Seconds to spend waiting on replica DB lag to resolve */
private $waitTimeout;
/** @var array The LoadMonitor configuration */
@ -116,6 +116,8 @@ class LoadBalancer implements ILoadBalancer {
private $readIndexByGroup = [];
/** @var bool|DBMasterPos Replication sync position or false if not set */
private $waitForPos;
/** @var bool Whether to disregard replica DB lag as a factor in replica DB selection */
private $allowLagged = false;
/** @var bool Whether the generic reader fell back to a lagged replica DB */
private $laggedReplicaMode = false;
/** @var string The last DB selection or connection error */
@ -134,6 +136,11 @@ class LoadBalancer implements ILoadBalancer {
/** @var int|null Integer ID of the managing LBFactory instance or null if none */
private $ownerId;
private static $INFO_SERVER_INDEX = 'serverIndex';
private static $INFO_AUTOCOMMIT_ONLY = 'autoCommitOnly';
private static $INFO_FORIEGN = 'foreign';
private static $INFO_FOREIGN_REF_COUNT = 'foreignPoolRefCount';
/** @var int Warn when this many connection are held */
const CONN_HELD_WARN_THRESHOLD = 10;
@ -170,6 +177,13 @@ class LoadBalancer implements ILoadBalancer {
throw new InvalidArgumentException( 'Missing or empty "servers" parameter' );
}
$localDomain = isset( $params['localDomain'] )
? DatabaseDomain::newFromId( $params['localDomain'] )
: DatabaseDomain::newUnspecified();
$this->setLocalDomain( $localDomain );
$this->maxLag = $params['maxLag'] ?? self::MAX_LAG_DEFAULT;
$listKey = -1;
$this->servers = [];
$this->groupLoads = [ self::GROUP_GENERIC => [] ];
@ -177,35 +191,22 @@ class LoadBalancer implements ILoadBalancer {
if ( ++$listKey !== $i ) {
throw new UnexpectedValueException( 'List expected for "servers" parameter' );
}
if ( $i == 0 ) {
$server['master'] = true;
} else {
$server['replica'] = true;
}
$this->servers[$i] = $server;
foreach ( ( $server['groupLoads'] ?? [] ) as $group => $ratio ) {
$this->groupLoads[$group][$i] = $ratio;
}
$this->groupLoads[self::GROUP_GENERIC][$i] = $server['load'];
$this->maxLagByIndex[$i] = $server['max lag'] ?? $this->maxLag;
}
$localDomain = isset( $params['localDomain'] )
? DatabaseDomain::newFromId( $params['localDomain'] )
: DatabaseDomain::newUnspecified();
$this->setLocalDomain( $localDomain );
$this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT;
$this->conns = self::newTrackedConnectionsArray();
$this->waitForPos = false;
$this->allowLagged = false;
if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
$this->readOnlyReason = $params['readOnlyReason'];
}
$this->maxLag = $params['maxLag'] ?? self::MAX_LAG_DEFAULT;
$this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
$this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
@ -774,7 +775,7 @@ class LoadBalancer implements ILoadBalancer {
$autocommit &&
(
// Connection is transaction round aware
!$candidateConn->getLBInfo( 'autoCommitOnly' ) ||
!$candidateConn->getLBInfo( self::$INFO_AUTOCOMMIT_ONLY ) ||
// Some sort of error left a transaction open?
$candidateConn->trxLevel()
)
@ -900,12 +901,12 @@ class LoadBalancer implements ILoadBalancer {
if (
$serverIndex === $this->getWriterIndex() &&
$this->getLaggedReplicaMode( $domain ) &&
!is_string( $conn->getLBInfo( 'readOnlyReason' ) )
!is_string( $conn->getLBInfo( $conn::LB_READ_ONLY_REASON ) )
) {
$reason = ( $this->getExistingReaderIndex( self::GROUP_GENERIC ) >= 0 )
? 'The database is read-only until replication lag decreases.'
: 'The database is read-only until replica database servers becomes reachable.';
$conn->setLBInfo( 'readOnlyReason', $reason );
$conn->setLBInfo( $conn::LB_READ_ONLY_REASON, $reason );
}
return $conn;
@ -965,15 +966,15 @@ class LoadBalancer implements ILoadBalancer {
} else {
$readOnlyReason = false;
}
$conn->setLBInfo( 'readOnlyReason', $readOnlyReason );
$conn->setLBInfo( $conn::LB_READ_ONLY_REASON, $readOnlyReason );
}
return $conn;
}
public function reuseConnection( IDatabase $conn ) {
$serverIndex = $conn->getLBInfo( 'serverIndex' );
$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
$serverIndex = $conn->getLBInfo( self::$INFO_SERVER_INDEX );
$refCount = $conn->getLBInfo( self::$INFO_FOREIGN_REF_COUNT );
if ( $serverIndex === null || $refCount === null ) {
return; // non-foreign connection; no domain-use tracking to update
} elseif ( $conn instanceof DBConnRef ) {
@ -990,7 +991,7 @@ class LoadBalancer implements ILoadBalancer {
return; // DBConnRef handle probably survived longer than the LoadBalancer
}
if ( $conn->getLBInfo( 'autoCommitOnly' ) ) {
if ( $conn->getLBInfo( self::$INFO_AUTOCOMMIT_ONLY ) ) {
$connFreeKey = self::KEY_FOREIGN_FREE_NOROUND;
$connInUseKey = self::KEY_FOREIGN_INUSE_NOROUND;
} else {
@ -1007,7 +1008,7 @@ class LoadBalancer implements ILoadBalancer {
"Connection $serverIndex/$domain mismatched; it may have already been freed" );
}
$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
$conn->setLBInfo( self::$INFO_FOREIGN_REF_COUNT, --$refCount );
if ( $refCount <= 0 ) {
$this->conns[$connFreeKey][$serverIndex][$domain] = $conn;
unset( $this->conns[$connInUseKey][$serverIndex][$domain] );
@ -1072,47 +1073,45 @@ class LoadBalancer implements ILoadBalancer {
*
* @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
*
* @param int $i Server index
* @param int $i Specific server index
* @param int $flags Class CONN_* constant bitfield
* @return Database
* @throws InvalidArgumentException When the server index is invalid
* @throws UnexpectedValueException When the DB domain of the connection is corrupted
*/
private function getLocalConnection( $i, $flags = 0 ) {
$autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
// Connection handles required to be in auto-commit mode use a separate connection
// pool since the main pool is effected by implicit and explicit transaction rounds
$autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
$connKey = $autoCommit ? self::KEY_LOCAL_NOROUND : self::KEY_LOCAL;
if ( isset( $this->conns[$connKey][$i][0] ) ) {
$conn = $this->conns[$connKey][$i][0];
} else {
// Open a new connection
$server = $this->getServerInfoStrict( $i );
$server['serverIndex'] = $i;
$server['autoCommitOnly'] = $autoCommit;
$conn = $this->reallyOpenConnection( $server, $this->localDomain );
$host = $this->getServerName( $i );
$conn = $this->reallyOpenConnection(
$i,
$this->localDomain,
[ self::$INFO_AUTOCOMMIT_ONLY => $autoCommit ]
);
if ( $conn->isOpen() ) {
$this->connLogger->debug(
__METHOD__ . ": connected to database $i at '$host'." );
$this->connLogger->debug( __METHOD__ . ": opened new connection for $i" );
$this->conns[$connKey][$i][0] = $conn;
} else {
$this->connLogger->warning(
__METHOD__ . ": failed to connect to database $i at '$host'." );
$this->connLogger->warning( __METHOD__ . ": connection error for $i" );
$this->errorConnection = $conn;
$conn = false;
}
}
// Final sanity check to make sure the right domain is selected
// Sanity check to make sure that the right domain is selected
if (
$conn instanceof IDatabase &&
!$this->localDomain->isCompatible( $conn->getDomainID() )
) {
throw new UnexpectedValueException(
"Got connection to '{$conn->getDomainID()}', " .
"but expected local domain ('{$this->localDomain}')" );
"but expected local domain ('{$this->localDomain}')"
);
}
return $conn;
@ -1134,7 +1133,7 @@ class LoadBalancer implements ILoadBalancer {
*
* @note If disable() was called on this LoadBalancer, this method will throw a DBAccessError.
*
* @param int $i Server index
* @param int $i Specific server index
* @param string $domain Domain ID to open
* @param int $flags Class CONN_* constant bitfield
* @return Database|bool Returns false on connection error
@ -1144,10 +1143,9 @@ class LoadBalancer implements ILoadBalancer {
*/
private function getForeignConnection( $i, $domain, $flags = 0 ) {
$domainInstance = DatabaseDomain::newFromId( $domain );
$autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
// Connection handles required to be in auto-commit mode use a separate connection
// pool since the main pool is effected by implicit and explicit transaction rounds
$autoCommit = ( ( $flags & self::CONN_TRX_AUTOCOMMIT ) == self::CONN_TRX_AUTOCOMMIT );
if ( $autoCommit ) {
$connFreeKey = self::KEY_FOREIGN_FREE_NOROUND;
$connInUseKey = self::KEY_FOREIGN_INUSE_NOROUND;
@ -1200,33 +1198,35 @@ class LoadBalancer implements ILoadBalancer {
}
if ( !$conn ) {
// Open a new connection
$server = $this->getServerInfoStrict( $i );
$server['serverIndex'] = $i;
$server['foreignPoolRefCount'] = 0;
$server['foreign'] = true;
$server['autoCommitOnly'] = $autoCommit;
$conn = $this->reallyOpenConnection( $server, $domainInstance );
if ( !$conn->isOpen() ) {
$this->connLogger->warning( __METHOD__ . ": connection error for $i/$domain" );
$this->errorConnection = $conn;
$conn = false;
} else {
$conn = $this->reallyOpenConnection(
$i,
$domainInstance,
[
self::$INFO_AUTOCOMMIT_ONLY => $autoCommit,
self::$INFO_FORIEGN => true,
self::$INFO_FOREIGN_REF_COUNT => 0
]
);
if ( $conn->isOpen() ) {
// Note that if $domain is an empty string, getDomainID() might not match it
$this->conns[$connInUseKey][$i][$conn->getDomainID()] = $conn;
$this->connLogger->debug( __METHOD__ . ": opened new connection for $i/$domain" );
} else {
$this->connLogger->warning( __METHOD__ . ": connection error for $i/$domain" );
$this->errorConnection = $conn;
$conn = false;
}
}
if ( $conn instanceof IDatabase ) {
// Final sanity check to make sure the right domain is selected
// Sanity check to make sure that the right domain is selected
if ( !$domainInstance->isCompatible( $conn->getDomainID() ) ) {
throw new UnexpectedValueException(
"Got connection to '{$conn->getDomainID()}', but expected '$domain'" );
}
// Increment reference count
$refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
$conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
$refCount = $conn->getLBInfo( self::$INFO_FOREIGN_REF_COUNT );
$conn->setLBInfo( self::$INFO_FOREIGN_REF_COUNT, $refCount + 1 );
}
return $conn;
@ -1254,75 +1254,65 @@ class LoadBalancer implements ILoadBalancer {
*
* Returns a Database object whether or not the connection was successful.
*
* @param array $server
* @param int $i Specific server index
* @param DatabaseDomain $domain Domain the connection is for, possibly unspecified
* @param array $lbInfo Additional information for setLBInfo()
* @return Database
* @throws DBAccessError
* @throws InvalidArgumentException
*/
protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
protected function reallyOpenConnection( $i, DatabaseDomain $domain, array $lbInfo ) {
if ( $this->disabled ) {
throw new DBAccessError();
}
if ( $domain->getDatabase() === null ) {
// The database domain does not specify a DB name and some database systems require a
// valid DB specified on connection. The $server configuration array contains a default
// DB name to use for connections in such cases.
if ( $server['type'] === 'mysql' ) {
// For MySQL, DATABASE and SCHEMA are synonyms, connections need not specify a DB,
// and the DB name in $server might not exist due to legacy reasons (the default
// domain used to ignore the local LB domain, even when mismatched).
$server['dbname'] = null;
}
} else {
$server['dbname'] = $domain->getDatabase();
}
$server = $this->getServerInfoStrict( $i );
if ( $domain->getSchema() !== null ) {
$server['schema'] = $domain->getSchema();
}
// It is always possible to connect with any prefix, even the empty string
$server['tablePrefix'] = $domain->getTablePrefix();
// Let the handle know what the cluster master is (e.g. "db1052")
$masterName = $this->getServerName( $this->getWriterIndex() );
$server['clusterMasterHost'] = $masterName;
$server['srvCache'] = $this->srvCache;
// Set loggers and profilers
$server['connLogger'] = $this->connLogger;
$server['queryLogger'] = $this->queryLogger;
$server['errorLogger'] = $this->errorLogger;
$server['deprecationLogger'] = $this->deprecationLogger;
$server['profiler'] = $this->profiler;
$server['trxProfiler'] = $this->trxProfiler;
// Use the same agent and PHP mode for all DB handles
$server['cliMode'] = $this->cliMode;
$server['agent'] = $this->agent;
// Use DBO_DEFAULT flags by default for LoadBalancer managed databases. Assume that the
// application calls LoadBalancer::commitMasterChanges() before the PHP script completes.
$server['flags'] = $server['flags'] ?? IDatabase::DBO_DEFAULT;
$server['ownerId'] = $this->id;
// Create a live connection object
$conn = Database::factory( $server['type'], $server, Database::NEW_UNCONNECTED );
$conn->setLBInfo( $server );
$conn->setLazyMasterHandle(
$this->getLazyConnectionRef( self::DB_MASTER, [], $conn->getDomainID() )
$conn = Database::factory(
$server['type'],
array_merge( $server, [
// Basic replication role information
'topologyRole' => $this->getTopologyRole( $i, $server ),
'topologicalMaster' => $this->getMasterServerName(),
// Use the database specified in $domain (null means "none or entrypoint DB");
// fallback to the $server default if the RDBMs is an embedded library using a
// file on disk since there would be nothing to access to without a DB/file name.
'dbname' => $this->getServerAttributes( $i )[Database::ATTR_DB_IS_FILE]
? ( $domain->getDatabase() ?? $server['dbname'] ?? null )
: $domain->getDatabase(),
// Override the $server default schema with that of $domain if specified
'schema' => $domain->getSchema() ?? $server['schema'] ?? null,
// Use the table prefix specified in $domain
'tablePrefix' => $domain->getTablePrefix(),
// Participate in transaction rounds if $server does not specify otherwise
'flags' => $this->initConnFlags( $server['flags'] ?? IDatabase::DBO_DEFAULT ),
// Inject the PHP execution mode and the agent string
'cliMode' => $this->cliMode,
'agent' => $this->agent,
'ownerId' => $this->id,
// Inject object and callback dependencies
'lazyMasterHandle' => $this->getLazyConnectionRef(
self::DB_MASTER,
[],
$domain->getId()
),
'srvCache' => $this->srvCache,
'connLogger' => $this->connLogger,
'queryLogger' => $this->queryLogger,
'errorLogger' => $this->errorLogger,
'deprecationLogger' => $this->deprecationLogger,
'profiler' => $this->profiler,
'trxProfiler' => $this->trxProfiler
] ),
Database::NEW_UNCONNECTED
);
// Attach load balancer information to the handle
$conn->setLBInfo( [ self::$INFO_SERVER_INDEX => $i ] + $lbInfo );
// Set alternative table/index names before any queries can be issued
$conn->setTableAliases( $this->tableAliases );
$conn->setIndexAliases( $this->indexAliases );
try {
$conn->initConnection();
++$this->connectionCounter;
} catch ( DBConnectionError $e ) {
// ignore; let the DB handle the logging
}
if ( $server['serverIndex'] === $this->getWriterIndex() ) {
// Account for any active transaction round and listeners
if ( $i === $this->getWriterIndex() ) {
if ( $this->trxRoundId !== false ) {
$this->applyTransactionRoundFlags( $conn );
}
@ -1331,9 +1321,22 @@ class LoadBalancer implements ILoadBalancer {
}
}
$this->lazyLoadReplicationPositions(); // session consistency
// Make the connection handle live
try {
$conn->initConnection();
++$this->connectionCounter;
} catch ( DBConnectionError $e ) {
// ignore; let the DB handle the logging
}
// Log when many connection are made on requests
// Try to maintain session consistency for clients that trigger write transactions
// in a request or script and then return soon after in another request or script.
// This requires cooperation with ChronologyProtector and the application wiring.
if ( $conn->isOpen() ) {
$this->lazyLoadReplicationPositions();
}
// Log when many connection are made during a single request/script
$count = $this->getCurrentConnectionCount();
if ( $count >= self::CONN_HELD_WARN_THRESHOLD ) {
$this->perfLogger->warning(
@ -1341,7 +1344,7 @@ class LoadBalancer implements ILoadBalancer {
[
'connections' => $count,
'dbserver' => $conn->getServer(),
'masterdb' => $conn->getLBInfo( 'clusterMasterHost' )
'masterdb' => $this->getMasterServerName()
]
);
}
@ -1349,6 +1352,38 @@ class LoadBalancer implements ILoadBalancer {
return $conn;
}
/**
* @param int $i Specific server index
* @param array $server Server config map
* @return string IDatabase::ROLE_* constant
*/
private function getTopologyRole( $i, array $server ) {
if ( !empty( $server['is static'] ) ) {
return IDatabase::ROLE_STATIC_CLONE;
}
return ( $i === $this->getWriterIndex() )
? IDatabase::ROLE_STREAMING_MASTER
: IDatabase::ROLE_STREAMING_REPLICA;
}
/**
* @see IDatabase::DBO_DEFAULT
* @param int $flags Bit field of IDatabase::DBO_* constants from configuration
* @return int Bit field of IDatabase::DBO_* constants to use with Database::factory()
*/
private function initConnFlags( $flags ) {
if ( ( $flags & IDatabase::DBO_DEFAULT ) === IDatabase::DBO_DEFAULT ) {
if ( $this->cliMode ) {
$flags &= ~IDatabase::DBO_TRX;
} else {
$flags |= IDatabase::DBO_TRX;
}
}
return $flags;
}
/**
* Make sure that any "waitForPos" positions are loaded and available to doWait()
*/
@ -1529,7 +1564,7 @@ class LoadBalancer implements ILoadBalancer {
throw new RuntimeException( 'Cannot close DBConnRef instance; it must be shareable' );
}
$serverIndex = $conn->getLBInfo( 'serverIndex' );
$serverIndex = $conn->getLBInfo( self::$INFO_SERVER_INDEX );
foreach ( $this->conns as $type => $connsByServer ) {
if ( !isset( $connsByServer[$serverIndex] ) ) {
continue;
@ -1871,7 +1906,7 @@ class LoadBalancer implements ILoadBalancer {
* @param Database $conn
*/
private function applyTransactionRoundFlags( Database $conn ) {
if ( $conn->getLBInfo( 'autoCommitOnly' ) ) {
if ( $conn->getLBInfo( self::$INFO_AUTOCOMMIT_ONLY ) ) {
return; // transaction rounds do not apply to these connections
}
@ -1882,7 +1917,7 @@ class LoadBalancer implements ILoadBalancer {
}
if ( $conn->getFlag( $conn::DBO_TRX ) ) {
$conn->setLBInfo( 'trxRoundId', $this->trxRoundId );
$conn->setLBInfo( $conn::LB_TRX_ROUND_ID, $this->trxRoundId );
}
}
@ -1890,12 +1925,12 @@ class LoadBalancer implements ILoadBalancer {
* @param Database $conn
*/
private function undoTransactionRoundFlags( Database $conn ) {
if ( $conn->getLBInfo( 'autoCommitOnly' ) ) {
if ( $conn->getLBInfo( self::$INFO_AUTOCOMMIT_ONLY ) ) {
return; // transaction rounds do not apply to these connections
}
if ( $conn->getFlag( $conn::DBO_TRX ) ) {
$conn->setLBInfo( 'trxRoundId', null ); // remove the round ID
$conn->setLBInfo( $conn::LB_TRX_ROUND_ID, null ); // remove the round ID
}
if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
@ -2192,20 +2227,14 @@ class LoadBalancer implements ILoadBalancer {
* @deprecated Since 1.34 Use IDatabase::getLag() instead
*/
public function safeGetLag( IDatabase $conn ) {
if ( $conn->getLBInfo( 'is static' ) ) {
return 0; // static dataset
} elseif ( $conn->getLBInfo( 'serverIndex' ) == $this->getWriterIndex() ) {
return 0; // this is the master
}
return $conn->getLag();
}
public function waitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {
$timeout = max( 1, $timeout ?: $this->waitTimeout );
if ( $this->getServerCount() <= 1 || !$conn->getLBInfo( 'replica' ) ) {
return true; // server is not a replica DB
if ( $conn->getLBInfo( self::$INFO_SERVER_INDEX ) === $this->getWriterIndex() ) {
return true; // not a replica DB server
}
if ( !$pos ) {
@ -2302,7 +2331,7 @@ class LoadBalancer implements ILoadBalancer {
$this->forEachOpenConnection( function ( IDatabase $conn ) use ( &$domainsInUse ) {
// Once reuseConnection() is called on a handle, its reference count goes from 1 to 0.
// Until then, it is still in use by the caller (explicitly or via DBConnRef scope).
if ( $conn->getLBInfo( 'foreignPoolRefCount' ) > 0 ) {
if ( $conn->getLBInfo( self::$INFO_FOREIGN_REF_COUNT ) > 0 ) {
$domainsInUse[] = $conn->getDomainID();
}
} );
@ -2322,7 +2351,7 @@ class LoadBalancer implements ILoadBalancer {
// Update the prefix for all local connections...
$this->forEachOpenConnection( function ( IDatabase $conn ) use ( $prefix ) {
if ( !$conn->getLBInfo( 'foreign' ) ) {
if ( !$conn->getLBInfo( self::$INFO_FORIEGN ) ) {
$conn->tablePrefix( $prefix );
}
} );
@ -2383,7 +2412,7 @@ class LoadBalancer implements ILoadBalancer {
}
/**
* @return string
* @return string Name of the master server of the relevant DB cluster (e.g. "db1052")
*/
private function getMasterServerName() {
return $this->getServerName( $this->getWriterIndex() );

View file

@ -37,21 +37,21 @@ class LoadBalancerSingle extends LoadBalancer {
* - connection: An IDatabase connection object
*/
public function __construct( array $params ) {
if ( !isset( $params['connection'] ) ) {
/** @var IDatabase $conn */
$conn = $params['connection'] ?? null;
if ( !$conn ) {
throw new InvalidArgumentException( "Missing 'connection' argument." );
}
$this->db = $params['connection'];
$this->db = $conn;
parent::__construct( [
'servers' => [
[
'type' => $this->db->getType(),
'host' => $this->db->getServer(),
'dbname' => $this->db->getDBname(),
'load' => 1,
]
],
'servers' => [ [
'type' => $conn->getType(),
'host' => $conn->getServer(),
'dbname' => $conn->getDBname(),
'load' => 1,
] ],
'trxProfiler' => $params['trxProfiler'] ?? null,
'srvCache' => $params['srvCache'] ?? null,
'wanCache' => $params['wanCache'] ?? null,
@ -60,7 +60,7 @@ class LoadBalancerSingle extends LoadBalancer {
] );
if ( isset( $params['readOnlyReason'] ) ) {
$this->db->setLBInfo( 'readOnlyReason', $params['readOnlyReason'] );
$conn->setLBInfo( $conn::LB_READ_ONLY_REASON, $params['readOnlyReason'] );
}
}
@ -78,7 +78,7 @@ class LoadBalancerSingle extends LoadBalancer {
) );
}
protected function reallyOpenConnection( array $server, DatabaseDomain $domain ) {
protected function reallyOpenConnection( $i, DatabaseDomain $domain, array $lbInfo = [] ) {
return $this->db;
}

View file

@ -24,6 +24,7 @@ use MediaWiki\Storage\SqlBlobStore;
use MediaWiki\User\UserIdentityValue;
use MediaWikiTestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\NullLogger;
use Revision;
use TestUserRegistry;
use Title;
@ -148,7 +149,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
->getMock();
$lb->method( 'reallyOpenConnection' )->willReturnCallback(
function ( array $server, $dbNameOverride ) {
function () use ( $server ) {
return $this->getDatabaseMock( $server );
}
);
@ -207,12 +208,15 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
'variables' => [],
'schema' => '',
'cliMode' => true,
'topologyRole' => Database::ROLE_STREAMING_MASTER,
'topologicalMaster' => null,
'agent' => '',
'load' => 100,
'srvCache' => new HashBagOStuff(),
'profiler' => null,
'trxProfiler' => new TransactionProfiler(),
'connLogger' => new \Psr\Log\NullLogger(),
'queryLogger' => new \Psr\Log\NullLogger(),
'connLogger' => new NullLogger(),
'queryLogger' => new NullLogger(),
'errorLogger' => function () {
},
'deprecationLogger' => function () {

View file

@ -184,6 +184,8 @@ class DatabasePostgresTest extends MediaWikiTestCase {
* @covers \Wikimedia\Rdbms\DatabasePostgres::getAttributes
*/
public function testAttributes() {
$this->assertTrue( DatabasePostgres::getAttributes()[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS] );
$this->assertTrue(
Database::attributesFromType( 'postgres' )[Database::ATTR_SCHEMAS_AS_TABLE_GROUPS]
);
}
}

View file

@ -52,8 +52,10 @@ class DatabaseTestHelper extends Database {
'schema' => null,
'tablePrefix' => '',
'flags' => 0,
'cliMode' => $opts['cliMode'] ?? true,
'cliMode' => true,
'agent' => '',
'topologyRole' => null,
'topologicalMaster' => null,
'srvCache' => new HashBagOStuff(),
'profiler' => null,
'trxProfiler' => new TransactionProfiler(),

View file

@ -23,6 +23,7 @@
* @copyright © 2013 Wikimedia Foundation Inc.
*/
use Wikimedia\AtEase\AtEase;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IMaintainableDatabase;
use Wikimedia\Rdbms\LBFactory;
@ -102,10 +103,12 @@ class LBFactoryTest extends MediaWikiTestCase {
$lb = $factory->getMainLB();
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
$dbw::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'master shows as master' );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
$this->assertEquals(
$dbr::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'replica shows as replica' );
$this->assertSame( 'my_test_wiki', $factory->resolveDomainID( 'my_test_wiki' ) );
$this->assertSame( $factory->getLocalDomainID(), $factory->resolveDomainID( false ) );
@ -146,18 +149,22 @@ class LBFactoryTest extends MediaWikiTestCase {
$lb = $factory->getMainLB();
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
$dbw::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'master shows as master' );
$this->assertEquals(
( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
$dbw->getLBInfo( 'clusterMasterHost' ),
$dbw->getTopologyRootMaster(),
'cluster master set' );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
$this->assertEquals(
$dbr::ROLE_STREAMING_REPLICA, $dbr->getTopologyRole(), 'replica shows as replica' );
$this->assertEquals(
( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
$dbr->getLBInfo( 'clusterMasterHost' ),
'cluster master set' );
$dbr->getTopologyRootMaster(),
'cluster master set'
);
$factory->shutdown();
}
@ -166,10 +173,12 @@ class LBFactoryTest extends MediaWikiTestCase {
$factory = $this->newLBFactoryMultiLBs();
$dbw = $factory->getMainLB()->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
$dbw::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'master shows as master' );
$dbr = $factory->getMainLB()->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
$this->assertEquals(
$dbr::ROLE_STREAMING_REPLICA, $dbr->getTopologyRole(), 'replica shows as replica' );
// Destructor should trigger without round stage errors
unset( $factory );
@ -473,7 +482,7 @@ class LBFactoryTest extends MediaWikiTestCase {
unset( $db );
/** @var IMaintainableDatabase $db */
$db = $lb->getConnection( DB_MASTER, [], '' );
$db = $lb->getConnection( DB_MASTER, [], $lb::DOMAIN_ANY );
$this->assertSame(
'',
@ -552,7 +561,7 @@ class LBFactoryTest extends MediaWikiTestCase {
);
$lb = $factory->getMainLB();
/** @var IMaintainableDatabase $db */
$db = $lb->getConnection( DB_MASTER, [], '' );
$db = $lb->getConnection( DB_MASTER, [], $lb::DOMAIN_ANY );
$this->assertSame( '', $db->getDomainID(), "Null domain used" );
@ -620,16 +629,16 @@ class LBFactoryTest extends MediaWikiTestCase {
);
$lb = $factory->getMainLB();
/** @var IDatabase $db */
$db = $lb->getConnection( DB_MASTER, [], '' );
$db = $lb->getConnection( DB_MASTER, [], $lb::DOMAIN_ANY );
\Wikimedia\suppressWarnings();
AtEase::suppressWarnings();
try {
$this->assertFalse( $db->selectDB( 'garbage-db' ) );
$this->assertFalse( $db->selectDomain( 'garbagedb' ) );
$this->fail( "No error thrown." );
} catch ( \Wikimedia\Rdbms\DBQueryError $e ) {
$this->assertRegExp( '/[\'"]garbage-db[\'"]/', $e->getMessage() );
$this->assertRegExp( '/[\'"]garbagedb[\'"]/', $e->getMessage() );
}
\Wikimedia\restoreWarnings();
AtEase::restoreWarnings();
}
/**
@ -648,12 +657,12 @@ class LBFactoryTest extends MediaWikiTestCase {
);
$lb = $factory->getMainLB();
if ( !$lb->getConnection( DB_MASTER )->databasesAreIndependent() ) {
$this->markTestSkipped( "Not applicable per databasesAreIndependent()" );
if ( !$factory->getMainLB()->getServerAttributes( 0 )[Database::ATTR_DB_IS_FILE] ) {
$this->markTestSkipped( "Not applicable per ATTR_DB_IS_FILE" );
}
/** @var IDatabase $db */
$lb->getConnection( DB_MASTER, [], '' );
$this->assertNotNull( $lb->getConnection( DB_MASTER, [], $lb::DOMAIN_ANY ) );
}
/**
@ -677,7 +686,7 @@ class LBFactoryTest extends MediaWikiTestCase {
}
$db = $lb->getConnection( DB_MASTER );
$db->selectDB( 'garbage-db' );
$db->selectDomain( 'garbage-db' );
}
/**

View file

@ -20,7 +20,6 @@
*
* @file
*/
use PHPUnit\Framework\Constraint\StringContains;
use Wikimedia\Rdbms\DBError;
use Wikimedia\Rdbms\DatabaseDomain;
@ -91,12 +90,15 @@ class LoadBalancerTest extends MediaWikiTestCase {
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $called );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
$dbw::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'master shows as master'
);
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
$this->assertWriteAllowed( $dbw );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'master' ), 'DB_REPLICA also gets the master' );
$this->assertEquals(
$dbr::ROLE_STREAMING_MASTER, $dbr->getTopologyRole(), 'DB_REPLICA also gets the master' );
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
@ -155,23 +157,26 @@ class LoadBalancerTest extends MediaWikiTestCase {
}
$dbw = $lb->getConnection( DB_MASTER );
$this->assertTrue( $dbw->getLBInfo( 'master' ), 'master shows as master' );
$this->assertEquals(
$dbw::ROLE_STREAMING_MASTER, $dbw->getTopologyRole(), 'master shows as master' );
$this->assertEquals(
( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
$dbw->getLBInfo( 'clusterMasterHost' ),
'cluster master set' );
$dbw->getTopologyRootMaster(),
'cluster master set'
);
$this->assertTrue( $dbw->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on master" );
$this->assertWriteAllowed( $dbw );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'replica' ), 'replica shows as replica' );
$this->assertEquals(
$dbr::ROLE_STREAMING_REPLICA, $dbr->getTopologyRole(), 'replica shows as replica' );
$this->assertTrue( $dbr->isReadOnly(), 'replica shows as replica' );
$this->assertEquals(
( $wgDBserver != '' ) ? $wgDBserver : 'localhost',
$dbr->getLBInfo( 'clusterMasterHost' ),
'cluster master set' );
$dbr->getTopologyRootMaster(),
'cluster master set'
);
$this->assertTrue( $dbr->getFlag( $dbw::DBO_TRX ), "DBO_TRX set on replica" );
$this->assertWriteForbidden( $dbr );
$this->assertEquals( $dbr->getLBInfo( 'serverIndex' ), $lb->getReaderIndex() );
if ( !$lb->getServerAttributes( $lb->getWriterIndex() )[$dbw::ATTR_DB_LEVEL_LOCKING] ) {
@ -575,7 +580,23 @@ class LoadBalancerTest extends MediaWikiTestCase {
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testForbiddenWritesNoRef() {
// Simulate web request with DBO_TRX
$lb = $this->newMultiServerLocalLoadBalancer( [], [ 'flags' => DBO_TRX ] );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->isReadOnly(), 'replica shows as replica' );
$dbr->delete( 'some_table', [ 'id' => 57634126 ], __METHOD__ );
$lb->closeAll();
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnection()
*/
public function testDBConnRefReadsMasterAndReplicaRoles() {
@ -602,7 +623,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRole() {
@ -614,7 +635,7 @@ class LoadBalancerTest extends MediaWikiTestCase {
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRoleIndex() {
@ -626,7 +647,19 @@ class LoadBalancerTest extends MediaWikiTestCase {
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testLazyDBConnRefWritesReplicaRoleIndex() {
$lb = $this->newMultiServerLocalLoadBalancer();
$rConn = $lb->getLazyConnectionRef( 1 );
$rConn->query( 'DELETE FROM sometesttable WHERE 1=0' );
}
/**
* @covers \Wikimedia\Rdbms\LoadBalancer::getConnectionRef()
* @expectedException \Wikimedia\Rdbms\DBReadOnlyRoleError
*/
public function testDBConnRefWritesReplicaRoleInsert() {

View file

@ -370,6 +370,8 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
$db->method( 'getMasterServerInfo' )
->willReturn( [ 'serverId' => 172, 'asOf' => time() ] );
$db->setLBInfo( 'replica', true );
// Fake the current time.
list( $nowSecFrac, $nowSec ) = explode( ' ', microtime() );
$now = (float)$nowSec + (float)$nowSecFrac;