server config or IDatabase) */ protected $dbServers; // (DB name => server config array) /** @var BagOStuff */ protected $statusCache; protected $lockExpiry; // integer number of seconds protected $safeDelay; // integer number of seconds /** @var IDatabase[] Map Database connections (DB name => Database) */ protected $conns = []; /** * Construct a new instance from configuration. * * @param array $config Parameters include: * - dbServers : Associative array of DB names to server configuration. * Configuration is an associative array that includes: * - host : DB server name * - dbname : DB name * - type : DB type (mysql,postgres,...) * - user : DB user * - password : DB user password * - tablePrefix : DB table prefix * - flags : DB flags; bitfield of IDatabase::DBO_* constants * - dbsByBucket : Array of 1-16 consecutive integer keys, starting from 0, * each having an odd-numbered list of DB names (peers) as values. * - lockExpiry : Lock timeout (seconds) for dropped connections. [optional] * This tells the DB server how long to wait before assuming * connection failure and releasing all the locks for a session. * - srvCache : A BagOStuff instance using APC or the like. */ public function __construct( array $config ) { parent::__construct( $config ); $this->dbServers = $config['dbServers']; // Sanitize srvsByBucket config to prevent PHP errors $this->srvsByBucket = array_filter( $config['dbsByBucket'], 'is_array' ); $this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive if ( isset( $config['lockExpiry'] ) ) { $this->lockExpiry = $config['lockExpiry']; } else { $met = ini_get( 'max_execution_time' ); $this->lockExpiry = $met ? $met : 60; // use some sane amount if 0 } $this->safeDelay = ( $this->lockExpiry <= 0 ) ? 60 // pick a safe-ish number to match DB timeout default : $this->lockExpiry; // cover worst case // Tracks peers that couldn't be queried recently to avoid lengthy // connection timeouts. This is useless if each bucket has one peer. $this->statusCache = isset( $config['srvCache'] ) ? $config['srvCache'] : new HashBagOStuff(); } /** * @TODO change this code to work in one batch * @param string $lockSrv * @param array $pathsByType * @return StatusValue */ protected function getLocksOnServer( $lockSrv, array $pathsByType ) { $status = StatusValue::newGood(); foreach ( $pathsByType as $type => $paths ) { $status->merge( $this->doGetLocksOnServer( $lockSrv, $paths, $type ) ); } return $status; } abstract protected function doGetLocksOnServer( $lockSrv, array $paths, $type ); protected function freeLocksOnServer( $lockSrv, array $pathsByType ) { return StatusValue::newGood(); } /** * @see QuorumLockManager::isServerUp() * @param string $lockSrv * @return bool */ protected function isServerUp( $lockSrv ) { if ( !$this->cacheCheckFailures( $lockSrv ) ) { return false; // recent failure to connect } try { $this->getConnection( $lockSrv ); } catch ( DBError $e ) { $this->cacheRecordFailure( $lockSrv ); return false; // failed to connect } return true; } /** * Get (or reuse) a connection to a lock DB * * @param string $lockDb * @return IDatabase * @throws DBError * @throws UnexpectedValueException */ protected function getConnection( $lockDb ) { if ( !isset( $this->conns[$lockDb] ) ) { if ( $this->dbServers[$lockDb] instanceof IDatabase ) { // Direct injected connection hande for $lockDB $db = $this->dbServers[$lockDb]; } elseif ( is_array( $this->dbServers[$lockDb] ) ) { // Parameters to construct a new database connection $config = $this->dbServers[$lockDb]; $db = Database::factory( $config['type'], $config ); } else { throw new UnexpectedValueException( "No server called '$lockDb'." ); } $db->clearFlag( DBO_TRX ); # If the connection drops, try to avoid letting the DB rollback # and release the locks before the file operations are finished. # This won't handle the case of DB server restarts however. $options = []; if ( $this->lockExpiry > 0 ) { $options['connTimeout'] = $this->lockExpiry; } $db->setSessionOptions( $options ); $this->initConnection( $lockDb, $db ); $this->conns[$lockDb] = $db; } return $this->conns[$lockDb]; } /** * Do additional initialization for new lock DB connection * * @param string $lockDb * @param IDatabase $db * @throws DBError */ protected function initConnection( $lockDb, IDatabase $db ) { } /** * Checks if the DB has not recently had connection/query errors. * This just avoids wasting time on doomed connection attempts. * * @param string $lockDb * @return bool */ protected function cacheCheckFailures( $lockDb ) { return ( $this->safeDelay > 0 ) ? !$this->statusCache->get( $this->getMissKey( $lockDb ) ) : true; } /** * Log a lock request failure to the cache * * @param string $lockDb * @return bool Success */ protected function cacheRecordFailure( $lockDb ) { return ( $this->safeDelay > 0 ) ? $this->statusCache->set( $this->getMissKey( $lockDb ), 1, $this->safeDelay ) : true; } /** * Get a cache key for recent query misses for a DB * * @param string $lockDb * @return string */ protected function getMissKey( $lockDb ) { return 'dblockmanager:downservers:' . str_replace( ' ', '_', $lockDb ); } /** * Make sure remaining locks get cleared for sanity */ function __destruct() { $this->releaseAllLocks(); foreach ( $this->conns as $db ) { $db->close(); } } }