private $mServers;
/** @var array[] Map of (local/foreignUsed/foreignFree => server index => IDatabase array) */
private $mConns;
- /** @var array Map of (server index => weight) */
+ /** @var float[] Map of (server index => weight) */
private $mLoads;
/** @var array[] Map of (group => server index => weight) */
private $mGroupLoads;
private $mAllowLagged;
/** @var integer Seconds to spend waiting on replica DB lag to resolve */
private $mWaitTimeout;
- /** @var string The LoadMonitor subclass name */
- private $mLoadMonitorClass;
+ /** @var array The LoadMonitor configuration */
+ private $loadMonitorConfig;
/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
private $tableAliases = [];
/** @var ILoadMonitor */
- private $mLoadMonitor;
+ private $loadMonitor;
/** @var BagOStuff */
private $srvCache;
/** @var BagOStuff */
}
if ( isset( $params['loadMonitor'] ) ) {
- $this->mLoadMonitorClass = $params['loadMonitor'];
+ $this->loadMonitorConfig = $params['loadMonitor'];
} else {
- $master = reset( $params['servers'] );
- if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) {
- $this->mLoadMonitorClass = 'LoadMonitorMySQL';
- } else {
- $this->mLoadMonitorClass = 'LoadMonitorNull';
- }
+ $this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ];
}
foreach ( $params['servers'] as $i => $server ) {
* @return ILoadMonitor
*/
private function getLoadMonitor() {
- if ( !isset( $this->mLoadMonitor ) ) {
- $class = $this->mLoadMonitorClass;
- $this->mLoadMonitor = new $class( $this, $this->srvCache, $this->memCache );
- $this->mLoadMonitor->setLogger( $this->replLogger );
+ if ( !isset( $this->loadMonitor ) ) {
+ $class = $this->loadMonitorConfig['class'];
+ $this->loadMonitor = new $class(
+ $this, $this->srvCache, $this->memCache, $this->loadMonitorConfig );
+ $this->loadMonitor->setLogger( $this->replLogger );
}
- return $this->mLoadMonitor;
+ return $this->loadMonitor;
}
/**
}
# Scale the configured load ratios according to the dynamic load if supported
- $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $domain );
+ $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $domain );
$laggedReplicaMode = false;
return $ok;
}
+ /**
+ * @see ILoadBalancer::getConnection()
+ *
+ * @param int $i
+ * @param array $groups
+ * @param bool $domain
+ * @return Database
+ * @throws DBConnectionError
+ */
public function getConnection( $i, $groups = [], $domain = false ) {
if ( $i === null || $i === false ) {
throw new InvalidArgumentException( 'Attempt to call ' . __METHOD__ .
? [ false ] // check one "group": the generic pool
: (array)$groups;
- $masterOnly = ( $i == DB_MASTER || $i == $this->getWriterIndex() );
+ $masterOnly = ( $i == self::DB_MASTER || $i == $this->getWriterIndex() );
$oldConnsOpened = $this->connsOpened; // connections open now
- if ( $i == DB_MASTER ) {
+ if ( $i == self::DB_MASTER ) {
$i = $this->getWriterIndex();
} else {
# Try to find an available server in any the query groups (in order)
}
# Operation-based index
- if ( $i == DB_REPLICA ) {
+ if ( $i == self::DB_REPLICA ) {
$this->mLastError = 'Unknown error'; // reset error string
# Try the general server pool if $groups are unavailable.
$i = in_array( false, $groups, true )
# Couldn't find a working server in getReaderIndex()?
if ( $i === false ) {
$this->mLastError = 'No working replica DB server: ' . $this->mLastError;
-
- return $this->reportConnectionError();
+ // Throw an exception
+ $this->reportConnectionError();
+ return null; // not reached
}
}
# Now we have an explicit index into the servers array
$conn = $this->openConnection( $i, $domain );
if ( !$conn ) {
- return $this->reportConnectionError();
+ // Throw an exception
+ $this->reportConnectionError();
+ return null; // not reached
}
# Profile any new connections that happen
/**
* This can happen in code like:
* foreach ( $dbs as $db ) {
- * $conn = $lb->getConnection( DB_REPLICA, [], $db );
+ * $conn = $lb->getConnection( $lb::DB_REPLICA, [], $db );
* ...
* $lb->reuseConnection( $conn );
* }
}
$domain = $conn->getDomainID();
- if ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) {
+ if ( !isset( $this->mConns['foreignUsed'][$serverIndex][$domain] ) ) {
+ throw new InvalidArgumentException( __METHOD__ .
+ ": connection $serverIndex/$domain not found; it may have already been freed." );
+ } elseif ( $this->mConns['foreignUsed'][$serverIndex][$domain] !== $conn ) {
throw new InvalidArgumentException( __METHOD__ .
- ": connection not found, has the connection been freed already?" );
+ ": connection $serverIndex/$domain mismatched; it may have already been freed." );
}
$conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
if ( $refCount <= 0 ) {
return new DBConnRef( $this, [ $db, $groups, $domain ] );
}
+ /**
+ * @see ILoadBalancer::openConnection()
+ *
+ * @param int $i
+ * @param bool $domain
+ * @return bool|Database
+ * @throws DBAccessError
+ */
public function openConnection( $i, $domain = false ) {
if ( $this->localDomain->equals( $domain ) || $domain === $this->localDomainIdAlias ) {
$domain = false; // local connection requested
*
* @param int $i Server index
* @param string $domain Domain ID to open
- * @return IDatabase
+ * @return Database
*/
private function openForeignConnection( $i, $domain ) {
$domainInstance = DatabaseDomain::newFromId( $domain );
*
* @param array $server
* @param string|bool $dbNameOverride Use "" to not select any database
- * @return IDatabase
+ * @return Database
* @throws DBAccessError
* @throws InvalidArgumentException
*/
$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'] = isset( $server['flags'] ) ? $server['flags'] : DBO_DEFAULT;
+ $server['flags'] = isset( $server['flags'] ) ? $server['flags'] : IDatabase::DBO_DEFAULT;
// Create a live connection object
try {
$db->setLBInfo( $server );
$db->setLazyMasterHandle(
- $this->getLazyConnectionRef( DB_MASTER, [], $db->getDomainID() )
+ $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
);
$db->setTableAliases( $this->tableAliases );
/**
* @throws DBConnectionError
- * @return bool
*/
private function reportConnectionError() {
- $conn = $this->mErrorConnection; // The connection which caused the error
+ $conn = $this->mErrorConnection; // the connection which caused the error
$context = [
'method' => __METHOD__,
'last_error' => $this->mLastError,
// throws DBConnectionError
$conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
}
-
- return false; /* not reached */
}
public function getWriterIndex() {
for ( $i = 1; $i < $serverCount; $i++ ) {
$conn = $this->getAnyOpenConnection( $i );
if ( $conn ) {
- return $conn->getSlavePos();
+ return $conn->getReplicaPos();
}
}
} else {
}
public function finalizeMasterChanges() {
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+ $this->forEachOpenMasterConnection( function ( Database $conn ) {
// Any error should cause all DB transactions to be rolled back together
$conn->setTrxEndCallbackSuppression( false );
$conn->runOnTransactionPreCommitCallbacks();
$failures = [];
$this->forEachOpenMasterConnection(
- function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
+ function ( Database $conn ) use ( $fname, &$failures ) {
$conn->setTrxEndCallbackSuppression( true );
try {
$conn->flushSnapshot( $fname );
public function commitMasterChanges( $fname = __METHOD__ ) {
$failures = [];
+ /** @noinspection PhpUnusedLocalVariableInspection */
+ $scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
+
$restore = ( $this->trxRoundId !== false );
$this->trxRoundId = false;
$this->forEachOpenMasterConnection(
public function runMasterPostTrxCallbacks( $type ) {
$e = null; // first exception
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $type, &$e ) {
+ $this->forEachOpenMasterConnection( function ( Database $conn ) use ( $type, &$e ) {
$conn->setTrxEndCallbackSuppression( false );
if ( $conn->writesOrCallbacksPending() ) {
// This happens if onTransactionIdle() callbacks leave callbacks on *another* DB
}
public function suppressTransactionEndCallbacks() {
- $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+ $this->forEachOpenMasterConnection( function ( Database $conn ) {
$conn->setTrxEndCallbackSuppression( true );
} );
}
* @param IDatabase $conn
*/
private function applyTransactionRoundFlags( IDatabase $conn ) {
- if ( $conn->getFlag( DBO_DEFAULT ) ) {
+ if ( $conn->getFlag( $conn::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.
- $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
+ $conn->setFlag( $conn::DBO_TRX, $conn::REMEMBER_PRIOR );
// If config has explicitly requested DBO_TRX be either on or off by not
// setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
// for things like blob stores (ExternalStore) which want auto-commit mode.
* @param IDatabase $conn
*/
private function undoTransactionRoundFlags( IDatabase $conn ) {
- if ( $conn->getFlag( DBO_DEFAULT ) ) {
+ if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
$conn->restoreFlags( $conn::RESTORE_PRIOR );
}
}
if ( !$this->laggedReplicaMode && $this->getServerCount() > 1 ) {
try {
// See if laggedReplicaMode gets set
- $conn = $this->getConnection( DB_REPLICA, false, $domain );
+ $conn = $this->getConnection( self::DB_REPLICA, false, $domain );
$this->reuseConnection( $conn );
} catch ( DBConnectionError $e ) {
// Avoid expensive re-connect attempts and failures
function () use ( $domain, $conn ) {
$this->trxProfiler->setSilenced( true );
try {
- $dbw = $conn ?: $this->getConnection( DB_MASTER, [], $domain );
+ $dbw = $conn ?: $this->getConnection( self::DB_MASTER, [], $domain );
$readOnly = (int)$dbw->serverIsReadOnly();
if ( !$conn ) {
$this->reuseConnection( $dbw );
if ( !$pos ) {
// Get the current master position
- $dbw = $this->getConnection( DB_MASTER );
+ $dbw = $this->getConnection( self::DB_MASTER );
$pos = $dbw->getMasterPos();
$this->reuseConnection( $dbw );
}
return $ok;
}
- public function clearLagTimeCache() {
- $this->getLoadMonitor()->clearCaches();
- }
-
public function setTransactionListener( $name, callable $callback = null ) {
if ( $callback ) {
$this->trxRecurringCallbacks[$name] = $callback;
} );
}
+ /**
+ * Make PHP ignore user aborts/disconnects until the returned
+ * value leaves scope. This returns null and does nothing in CLI mode.
+ *
+ * @return ScopedCallback|null
+ */
+ final protected function getScopedPHPBehaviorForCommit() {
+ if ( PHP_SAPI != 'cli' ) { // http://bugs.php.net/bug.php?id=47540
+ $old = ignore_user_abort( true ); // avoid half-finished operations
+ return new ScopedCallback( function () use ( $old ) {
+ ignore_user_abort( $old );
+ } );
+ }
+
+ return null;
+ }
+
function __destruct() {
// Avoid connection leaks for sanity
$this->closeAll();