Add more information to reuseConnection() exceptions
[lhc/web/wiklou.git] / includes / libs / rdbms / loadbalancer / LoadBalancer.php
index bda185a..66e4fcf 100644 (file)
@@ -32,7 +32,7 @@ class LoadBalancer implements ILoadBalancer {
        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;
@@ -40,13 +40,13 @@ class LoadBalancer implements ILoadBalancer {
        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 */
@@ -150,14 +150,9 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                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 ) {
@@ -217,13 +212,14 @@ class LoadBalancer implements ILoadBalancer {
         * @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;
        }
 
        /**
@@ -305,7 +301,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                # 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;
 
@@ -516,6 +512,15 @@ class LoadBalancer implements ILoadBalancer {
                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__ .
@@ -530,10 +535,10 @@ class LoadBalancer implements ILoadBalancer {
                        ? [ 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)
@@ -547,7 +552,7 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                # 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 )
@@ -556,15 +561,18 @@ class LoadBalancer implements ILoadBalancer {
                        # 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
@@ -589,7 +597,7 @@ class LoadBalancer implements ILoadBalancer {
                        /**
                         * 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 );
                         *   }
@@ -607,9 +615,12 @@ class LoadBalancer implements ILoadBalancer {
                }
 
                $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 ) {
@@ -637,6 +648,14 @@ class LoadBalancer implements ILoadBalancer {
                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
@@ -691,7 +710,7 @@ class LoadBalancer implements ILoadBalancer {
         *
         * @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 );
@@ -775,7 +794,7 @@ class LoadBalancer implements ILoadBalancer {
         *
         * @param array $server
         * @param string|bool $dbNameOverride Use "" to not select any database
-        * @return IDatabase
+        * @return Database
         * @throws DBAccessError
         * @throws InvalidArgumentException
         */
@@ -816,7 +835,7 @@ class LoadBalancer implements ILoadBalancer {
                $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 {
@@ -829,7 +848,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $db->setLBInfo( $server );
                $db->setLazyMasterHandle(
-                       $this->getLazyConnectionRef( DB_MASTER, [], $db->getDomainID() )
+                       $this->getLazyConnectionRef( self::DB_MASTER, [], $db->getDomainID() )
                );
                $db->setTableAliases( $this->tableAliases );
 
@@ -847,10 +866,9 @@ class LoadBalancer implements ILoadBalancer {
 
        /**
         * @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,
@@ -875,8 +893,6 @@ class LoadBalancer implements ILoadBalancer {
                        // throws DBConnectionError
                        $conn->reportConnectionError( "{$this->mLastError} ({$context['db_server']})" );
                }
-
-               return false; /* not reached */
        }
 
        public function getWriterIndex() {
@@ -928,7 +944,7 @@ class LoadBalancer implements ILoadBalancer {
                        for ( $i = 1; $i < $serverCount; $i++ ) {
                                $conn = $this->getAnyOpenConnection( $i );
                                if ( $conn ) {
-                                       return $conn->getSlavePos();
+                                       return $conn->getReplicaPos();
                                }
                        }
                } else {
@@ -1007,7 +1023,7 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        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();
@@ -1060,7 +1076,7 @@ class LoadBalancer implements ILoadBalancer {
 
                $failures = [];
                $this->forEachOpenMasterConnection(
-                       function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
+                       function ( Database $conn ) use ( $fname, &$failures ) {
                                $conn->setTrxEndCallbackSuppression( true );
                                try {
                                        $conn->flushSnapshot( $fname );
@@ -1084,6 +1100,9 @@ class LoadBalancer implements ILoadBalancer {
        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(
@@ -1114,7 +1133,7 @@ class LoadBalancer implements ILoadBalancer {
 
        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
@@ -1161,7 +1180,7 @@ class LoadBalancer implements ILoadBalancer {
        }
 
        public function suppressTransactionEndCallbacks() {
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+               $this->forEachOpenMasterConnection( function ( Database $conn ) {
                        $conn->setTrxEndCallbackSuppression( true );
                } );
        }
@@ -1170,10 +1189,10 @@ class LoadBalancer implements ILoadBalancer {
         * @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.
@@ -1184,7 +1203,7 @@ class LoadBalancer implements ILoadBalancer {
         * @param IDatabase $conn
         */
        private function undoTransactionRoundFlags( IDatabase $conn ) {
-               if ( $conn->getFlag( DBO_DEFAULT ) ) {
+               if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
                        $conn->restoreFlags( $conn::RESTORE_PRIOR );
                }
        }
@@ -1238,7 +1257,7 @@ class LoadBalancer implements ILoadBalancer {
                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
@@ -1305,7 +1324,7 @@ class LoadBalancer implements ILoadBalancer {
                        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 );
@@ -1432,7 +1451,7 @@ class LoadBalancer implements ILoadBalancer {
 
                if ( !$pos ) {
                        // Get the current master position
-                       $dbw = $this->getConnection( DB_MASTER );
+                       $dbw = $this->getConnection( self::DB_MASTER );
                        $pos = $dbw->getMasterPos();
                        $this->reuseConnection( $dbw );
                }
@@ -1455,10 +1474,6 @@ class LoadBalancer implements ILoadBalancer {
                return $ok;
        }
 
-       public function clearLagTimeCache() {
-               $this->getLoadMonitor()->clearCaches();
-       }
-
        public function setTransactionListener( $name, callable $callback = null ) {
                if ( $callback ) {
                        $this->trxRecurringCallbacks[$name] = $callback;
@@ -1499,6 +1514,23 @@ class LoadBalancer implements ILoadBalancer {
                } );
        }
 
+       /**
+        * 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();