* Note that lag is still possible depending on how wsrep-sync-wait is set server-side.
* - Read-only archive clones: set 'is static' in the server configuration maps. This will
* treat all such DBs as having 0 lag.
+ * - Externally updated dataset clones: set 'is static' in the server configuration maps.
+ * This will treat all such DBs as having 0 lag.
* - SQL load balancing proxy: any proxy should handle lag checks on its own, so the 'max lag'
* parameter should probably be set to INF in the server configuration maps. This will make
* the load balancer ignore whatever it detects as the lag of the logical replica is (which
public function redefineLocalDomain( $domain );
/**
- * Get the index of the reader connection, which may be a replica DB
+ * Get the server index of the reader connection for a given group
*
* This takes into account load ratios and lag times. It should return a consistent
* index during the life time of the load balancer. This initially checks replica DBs
public function getMaintenanceConnectionRef( $i, $groups = [], $domain = false, $flags = 0 );
/**
+ * Get the server index of the master server
+ *
* @return int
*/
public function getWriterIndex();
public function isNonZeroLoad( $i );
/**
- * Get the number of defined servers (not the number of open connections)
+ * Get the number of servers defined in configuration
*
* @return int
*/
public function getServerCount();
+ /**
+ * Whether there are any replica servers configured
+ *
+ * This counts both servers using streaming replication from the master server and
+ * servers that just have a clone of the static dataset found on the master server
+ *
+ * @return int
+ * @since 1.34
+ */
+ public function hasReplicaServers();
+
+ /**
+ * Whether any replica servers use streaming replication from the master server
+ *
+ * Generally this is one less than getServerCount(), though it might otherwise
+ * return a lower number if some of the servers are configured with "is static".
+ * That flag is used when both the server has no active replication setup and the
+ * dataset is either read-only or occasionally updated out-of-band. For example,
+ * a script might import a new geographic information dataset each week by writing
+ * it to each server and later directing the application to use the new version.
+ *
+ * It is possible for some replicas to be configured with "is static" but not
+ * others, though it generally should either be set for all or none of the replicas.
+ *
+ * If this returns zero, this means that there is generally no reason to execute
+ * replication wait logic for session consistency and lag reduction.
+ *
+ * @return int
+ * @since 1.34
+ */
+ public function hasStreamingReplicaServers();
+
/**
* Get the host name or IP address of the server with the specified index
*
public function forEachOpenReplicaConnection( $callback, array $params = [] );
/**
- * Get the hostname and lag time of the most-lagged replica DB
+ * Get the hostname and lag time of the most-lagged replica server
*
* 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
const ROUND_ERROR = 'error';
public function __construct( array $params ) {
- if ( !isset( $params['servers'] ) ) {
- throw new InvalidArgumentException( __CLASS__ . ': missing "servers" parameter' );
+ if ( !isset( $params['servers'] ) || !count( $params['servers'] ) ) {
+ throw new InvalidArgumentException( 'Missing or empty "servers" parameter' );
}
- $this->servers = $params['servers'];
- foreach ( $this->servers as $i => $server ) {
+
+ $listKey = -1;
+ $this->servers = [];
+ $this->genericLoads = [];
+ foreach ( $params['servers'] as $i => $server ) {
+ if ( ++$listKey !== $i ) {
+ throw new UnexpectedValueException( 'List expected for "servers" parameter' );
+ }
if ( $i == 0 ) {
- $this->servers[$i]['master'] = true;
+ $server['master'] = true;
} else {
- $this->servers[$i]['replica'] = true;
+ $server['replica'] = true;
+ }
+ $this->servers[$i] = $server;
+
+ $this->genericLoads[$i] = $server['load'];
+ if ( isset( $server['groupLoads'] ) ) {
+ foreach ( $server['groupLoads'] as $group => $ratio ) {
+ $this->groupLoads[$group][$i] = $ratio;
+ }
}
}
$this->waitTimeout = $params['waitTimeout'] ?? self::MAX_WAIT_DEFAULT;
- $this->conns = [
- // Connection were transaction rounds may be applied
- self::KEY_LOCAL => [],
- self::KEY_FOREIGN_INUSE => [],
- self::KEY_FOREIGN_FREE => [],
- // Auto-committing counterpart connections that ignore transaction rounds
- self::KEY_LOCAL_NOROUND => [],
- self::KEY_FOREIGN_INUSE_NOROUND => [],
- self::KEY_FOREIGN_FREE_NOROUND => []
- ];
- $this->genericLoads = [];
+ $this->conns = self::newConnsArray();
$this->waitForPos = false;
$this->allowLagged = false;
$this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
$this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
- foreach ( $params['servers'] as $i => $server ) {
- $this->genericLoads[$i] = $server['load'];
- if ( isset( $server['groupLoads'] ) ) {
- foreach ( $server['groupLoads'] as $group => $ratio ) {
- if ( !isset( $this->groupLoads[$group] ) ) {
- $this->groupLoads[$group] = [];
- }
- $this->groupLoads[$group][$i] = $ratio;
- }
- }
- }
-
$this->srvCache = $params['srvCache'] ?? new EmptyBagOStuff();
$this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
$this->profiler = $params['profiler'] ?? null;
$this->ownerId = $params['ownerId'] ?? null;
}
+ private static function newConnsArray() {
+ return [
+ // Connection were transaction rounds may be applied
+ self::KEY_LOCAL => [],
+ self::KEY_FOREIGN_INUSE => [],
+ self::KEY_FOREIGN_FREE => [],
+ // Auto-committing counterpart connections that ignore transaction rounds
+ self::KEY_LOCAL_NOROUND => [],
+ self::KEY_FOREIGN_INUSE_NOROUND => [],
+ self::KEY_FOREIGN_FREE_NOROUND => []
+ ];
+ }
+
public function getLocalDomainID() {
return $this->localDomain->getId();
}
# Unset excessively lagged servers
foreach ( $lags as $i => $lag ) {
- if ( $i != 0 ) {
+ if ( $i !== $this->getWriterIndex() ) {
# How much lag this server nominally is allowed to have
$maxServerLag = $this->servers[$i]['max lag'] ?? $this->maxLag; // default
# Constrain that futher by $maxLag argument
}
public function getReaderIndex( $group = false, $domain = false ) {
- if ( count( $this->servers ) == 1 ) {
+ if ( $this->getServerCount() == 1 ) {
// Skip the load balancing if there's only one server
return $this->getWriterIndex();
}
// If data seen by queries is expected to reflect the transactions committed as of
// or after a given replication position then wait for the DB to apply those changes
- if ( $this->waitForPos && $i != $this->getWriterIndex() && !$this->doWait( $i ) ) {
+ if ( $this->waitForPos && $i !== $this->getWriterIndex() && !$this->doWait( $i ) ) {
// Data will be outdated compared to what was expected
$laggedReplicaMode = true;
}
// Any server with less lag than it's 'max lag' param is preferable
$i = $this->getRandomNonLagged( $currentLoads, $domain );
}
- if ( $i === false && count( $currentLoads ) != 0 ) {
+ if ( $i === false && count( $currentLoads ) ) {
// All replica DBs lagged. Switch to read-only mode
$this->replLogger->error(
__METHOD__ . ": all replica DBs lagged. Switch to read-only mode" );
$oldPos = $this->waitForPos;
try {
$this->waitForPos = $pos;
- $serverCount = count( $this->servers );
+ $serverCount = $this->getServerCount();
$ok = true;
for ( $i = 1; $i < $serverCount; $i++ ) {
}
/**
- * Wait for a given replica DB to catch up to the master pos stored in $this
+ * Wait for a given replica DB to catch up to the master pos stored in "waitForPos"
* @param int $index Server index
* @param bool $open Check the server even if a new connection has to be made
- * @param int|null $timeout Max seconds to wait; default is "waitTimeout" given to __construct()
+ * @param int|null $timeout Max seconds to wait; default is "waitTimeout"
* @return bool
*/
protected function doWait( $index, $open = false, $timeout = null ) {
$domain = $this->resolveDomainID( $domain );
$flags = $this->sanitizeConnectionFlags( $flags );
- $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() );
+ $masterOnly = ( $i === self::DB_MASTER || $i === $this->getWriterIndex() );
// Number of connections made before getting the server index and handle
$priorConnectionsMade = $this->connsOpened;
}
$this->enforceConnectionFlags( $conn, $flags );
- if ( $serverIndex == $this->getWriterIndex() ) {
+ if ( $serverIndex === $this->getWriterIndex() ) {
// If the load balancer is read-only, perhaps due to replication lag, then master
// DB handles will reflect that. Note that Database::assertIsWritableMaster() takes
// care of replica DB handles whereas getReadOnlyReason() would cause infinite loops.
return count( $this->servers );
}
+ public function hasReplicaServers() {
+ return ( $this->getServerCount() > 1 );
+ }
+
+ public function hasStreamingReplicaServers() {
+ foreach ( $this->servers as $i => $server ) {
+ if ( $i !== $this->getWriterIndex() && empty( $server['is static'] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public function getServerName( $i ) {
$name = $this->servers[$i]['hostName'] ?? $this->servers[$i]['host'] ?? '';
# master (however unlikely that may be), then we can fetch the position from the replica DB.
$masterConn = $this->getAnyOpenConnection( $this->getWriterIndex() );
if ( !$masterConn ) {
- $serverCount = count( $this->servers );
+ $serverCount = $this->getServerCount();
for ( $i = 1; $i < $serverCount; $i++ ) {
$conn = $this->getAnyOpenConnection( $i );
if ( $conn ) {
$conn->close();
} );
- $this->conns = [
- self::KEY_LOCAL => [],
- self::KEY_FOREIGN_INUSE => [],
- self::KEY_FOREIGN_FREE => [],
- self::KEY_LOCAL_NOROUND => [],
- self::KEY_FOREIGN_INUSE_NOROUND => [],
- self::KEY_FOREIGN_FREE_NOROUND => []
- ];
+ $this->conns = self::newConnsArray();
$this->connsOpened = 0;
}
public function getLaggedReplicaMode( $domain = false ) {
if (
// Avoid recursion if there is only one DB
- $this->getServerCount() > 1 &&
+ $this->hasStreamingReplicaServers() &&
// Avoid recursion if the (non-zero load) master DB was picked for generic reads
$this->genericReadIndex !== $this->getWriterIndex() &&
// Stay in lagged replica mode during the load balancer instance lifetime
}
public function getMaxLag( $domain = false ) {
- $maxLag = -1;
$host = '';
+ $maxLag = -1;
$maxIndex = 0;
- if ( $this->getServerCount() <= 1 ) {
- return [ $host, $maxLag, $maxIndex ]; // no replication = no lag
- }
-
- $lagTimes = $this->getLagTimes( $domain );
- foreach ( $lagTimes as $i => $lag ) {
- if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
- $maxLag = $lag;
- $host = $this->servers[$i]['host'];
- $maxIndex = $i;
+ if ( $this->hasReplicaServers() ) {
+ $lagTimes = $this->getLagTimes( $domain );
+ foreach ( $lagTimes as $i => $lag ) {
+ if ( $this->genericLoads[$i] > 0 && $lag > $maxLag ) {
+ $maxLag = $lag;
+ $host = $this->servers[$i]['host'];
+ $maxIndex = $i;
+ }
}
}
}
public function getLagTimes( $domain = false ) {
- if ( $this->getServerCount() <= 1 ) {
+ if ( !$this->hasReplicaServers() ) {
return [ $this->getWriterIndex() => 0 ]; // no replication = no lag
}
}
public function safeGetLag( IDatabase $conn ) {
- if ( $this->getServerCount() <= 1 ) {
- return 0;
- } else {
- return $conn->getLag();
+ 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 safeWaitForMasterPos( IDatabase $conn, $pos = false, $timeout = null ) {