* code formatting
[lhc/web/wiklou.git] / includes / LoadBalancer.php
index 5ed207c..40b4613 100644 (file)
@@ -19,6 +19,12 @@ define( 'DB_LAST', -3 );     # Whatever database was used last
 define( 'DB_READ', -1 );
 define( 'DB_WRITE', -2 );
 
+
+# Scale polling time so that under overload conditions, the database server
+# receives a SHOW STATUS query at an average interval of this many microseconds
+define( 'AVG_STATUS_POLL', 2000 );
+
+
 /**
  * Database load balancing object
  *
@@ -27,10 +33,10 @@ define( 'DB_WRITE', -2 );
  */
 class LoadBalancer {
        /* private */ var $mServers, $mConnections, $mLoads, $mGroupLoads;
-       /* private */ var $mFailFunction;
+       /* private */ var $mFailFunction, $mErrorConnection;
        /* private */ var $mForce, $mReadIndex, $mLastIndex;
        /* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout;
-       /* private */ var $mLaggedSlaveMode;
+       /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
 
        function LoadBalancer()
        {
@@ -40,6 +46,7 @@ class LoadBalancer {
                $this->mReadIndex = -1;
                $this->mForce = -1;
                $this->mLastIndex = -1;
+               $this->mErrorConnection = false;
        }
 
        function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
@@ -74,9 +81,9 @@ class LoadBalancer {
                                        $this->mGroupLoads[$group][$i] = $ratio;
                                }
                        }
-               }       
+               }
        }
-       
+
        /**
         * Given an array of non-normalised probabilities, this function will select
         * an element and return the appropriate key
@@ -101,7 +108,7 @@ class LoadBalancer {
                }
                $max = mt_getrandmax();
                $rand = mt_rand(0, $max) / $max * $sum;
-               
+
                $sum = 0;
                foreach ( $weights as $i => $w ) {
                        $sum += $w;
@@ -130,7 +137,7 @@ class LoadBalancer {
                if ( $sum == 0 ) {
                        # No appropriate DB servers except maybe the master and some slaves with zero load
                        # Do NOT use the master
-                       # Instead, this function will return false, triggering read-only mode, 
+                       # Instead, this function will return false, triggering read-only mode,
                        # and a lagged slave will be used instead.
                        unset ( $loads[0] );
                }
@@ -139,16 +146,21 @@ class LoadBalancer {
                        return false;
                }
 
-               #wfDebug( var_export( $loads, true ) );
+               #wfDebugLog( 'connect', var_export( $loads, true ) );
 
                # Return a random representative of the remainder
                return $this->pickRandom( $loads );
        }
 
-
-       function getReaderIndex()
-       {
-               global $wgMaxLag, $wgReadOnly;
+       /**
+        * Get the index of the reader connection, which may be a slave
+        * This takes into account load ratios and lag times. It should
+        * always return a consistent index during a given invocation
+        *
+        * Side effect: opens connections to databases
+        */
+       function getReaderIndex() {
+               global $wgReadOnly, $wgDBClusterTimeout;
 
                $fname = 'LoadBalancer::getReaderIndex';
                wfProfileIn( $fname );
@@ -176,21 +188,22 @@ class LoadBalancer {
                                                        $i = $this->pickRandom( $loads );
                                                }
                                        }
+                                       $serverIndex = $i;
                                        if ( $i !== false ) {
-                                               wfDebug( "Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+                                               wfDebugLog( 'connect', "Using reader #$i: {$this->mServers[$i]['host']}...\n" );
                                                $this->openConnection( $i );
-                                               
+
                                                if ( !$this->isOpen( $i ) ) {
                                                        wfDebug( "Failed\n" );
                                                        unset( $loads[$i] );
                                                        $sleepTime = 0;
                                                } else {
                                                        $status = $this->mConnections[$i]->getStatus();
-                                                       if ( isset( $this->mServers[$i]['max threads'] ) && 
-                                                         $status['Threads_running'] > $this->mServers[$i]['max threads'] ) 
+                                                       if ( isset( $this->mServers[$i]['max threads'] ) &&
+                                                         $status['Threads_running'] > $this->mServers[$i]['max threads'] )
                                                        {
                                                                # Slave is lagged, wait for a while
-                                                               $sleepTime = 5000 * $status['Threads_connected'];
+                                                               $sleepTime = AVG_STATUS_POLL * $status['Threads_connected'];
 
                                                                # If we reach the timeout and exit the loop, don't use it
                                                                $i = false;
@@ -204,12 +217,28 @@ class LoadBalancer {
                                        }
                                        if ( $sleepTime ) {
                                                        $totalElapsed += $sleepTime;
+                                                       $x = "{$this->mServers[$serverIndex]['host']} [$serverIndex]";
+                                                       wfProfileIn( "$fname-sleep $x" );
                                                        usleep( $sleepTime );
+                                                       wfProfileOut( "$fname-sleep $x" );
                                        }
-                               } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $this->mWaitTimeout );
+                               } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $wgDBClusterTimeout );
 
+                               if ( $totalElapsed / 1e6 >= $wgDBClusterTimeout ) {
+                                       $this->mErrorConnection = false;
+                                       $this->mLastError = 'All servers busy';
+                               }
+                               
                                if ( $i !== false && $this->isOpen( $i ) ) {
-                                       $this->mReadIndex = $i;
+                                       # Wait for the session master pos for a short time
+                                       if ( $this->mWaitForFile ) {
+                                               if ( !$this->doWait( $i ) ) {
+                                                       $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
+                                               }
+                                       }
+                                       if ( $i !== false ) {
+                                               $this->mReadIndex = $i;
+                                       }
                                } else {
                                        $i = false;
                                }
@@ -218,7 +247,7 @@ class LoadBalancer {
                wfProfileOut( $fname );
                return $i;
        }
-       
+
        /**
         * Get a random server to use in a query group
         */
@@ -231,14 +260,13 @@ class LoadBalancer {
                wfDebug( "Query group $group => $i\n" );
                return $i;
        }
-       
+
        /**
         * Set the master wait position
         * If a DB_SLAVE connection has been opened already, waits
         * Otherwise sets a variable telling it to wait if such a connection is opened
         */
        function waitFor( $file, $pos ) {
-               /*
                $fname = 'LoadBalancer::waitFor';
                wfProfileIn( $fname );
 
@@ -256,20 +284,17 @@ class LoadBalancer {
                                        $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
                                        $this->mLaggedSlaveMode = true;
                                }
-                       } 
+                       }
                }
                wfProfileOut( $fname );
-               */
        }
 
        /**
         * Wait for a given slave to catch up to the master pos stored in $this
         */
        function doWait( $index ) {
-               return true;
-               /*
                global $wgMemc;
-               
+
                $retVal = false;
 
                # Debugging hacks
@@ -303,8 +328,8 @@ class LoadBalancer {
                                wfDebug( "Done\n" );
                        }
                }
-               return $retVal;*/
-       }               
+               return $retVal;
+       }
 
        /**
         * Get a connection by index
@@ -313,7 +338,7 @@ class LoadBalancer {
        {
                $fname = 'LoadBalancer::getConnection';
                wfProfileIn( $fname );
-               
+
                # Query groups
                $groupIndex = false;
                foreach ( $groups as $group ) {
@@ -323,9 +348,9 @@ class LoadBalancer {
                                break;
                        }
                }
-               
+
                # Operation-based index
-               if ( $i == DB_SLAVE ) { 
+               if ( $i == DB_SLAVE ) {
                        $i = $this->getReaderIndex();
                } elseif ( $i == DB_MASTER ) {
                        $i = $this->getWriterIndex();
@@ -338,9 +363,13 @@ class LoadBalancer {
                                $i = $this->getWriterIndex();
                        }
                }
+               # Couldn't find a working server in getReaderIndex()?
+               if ( $i === false ) {
+                       $this->reportConnectionError( $this->mErrorConnection );
+               }
                # Now we have an explicit index into the servers array
                $this->openConnection( $i, $fail );
-               
+
                wfProfileOut( $fname );
                return $this->mConnections[$i];
        }
@@ -358,19 +387,14 @@ class LoadBalancer {
 
                if ( !$this->isOpen( $i ) ) {
                        $this->mConnections[$i] = $this->reallyOpenConnection( $this->mServers[$i] );
-
-                       if ( $this->isOpen( $i ) && $i != 0 && $this->mWaitForFile ) {
-                               if ( !$this->doWait( $i ) ) {
-                                       $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
-                                       $success = false;
-                               }
-                       }
                }
+
                if ( !$this->isOpen( $i ) ) {
                        wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
                        if ( $fail ) {
                                $this->reportConnectionError( $this->mConnections[$i] );
                        }
+                       $this->mErrorConnection = $this->mConnections[$i];
                        $this->mConnections[$i] = false;
                        $success = false;
                }
@@ -387,15 +411,15 @@ class LoadBalancer {
                if( !is_integer( $index ) ) {
                        return false;
                }
-               if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) && 
-                 $this->mConnections[$index]->isOpen() ) 
+               if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) &&
+                 $this->mConnections[$index]->isOpen() )
                {
                        return true;
                } else {
                        return false;
                }
        }
-       
+
        /**
         * Really opens a connection
         * @private
@@ -404,7 +428,7 @@ class LoadBalancer {
                if( !is_array( $server ) ) {
                        wfDebugDieBacktrace( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
                }
-               
+
                extract( $server );
                # Get class for this database type
                $class = 'Database' . ucfirst( $type );
@@ -413,32 +437,43 @@ class LoadBalancer {
                }
 
                # Create object
-               return new $class( $host, $user, $password, $dbname, 1, $flags );
+               $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+               $db->setLBInfo( $server );
+               return $db;
        }
-       
+
        function reportConnectionError( &$conn )
        {
                $fname = 'LoadBalancer::reportConnectionError';
                wfProfileIn( $fname );
                # Prevent infinite recursion
-               
+
                static $reporting = false;
                if ( !$reporting ) {
                        $reporting = true;
                        if ( !is_object( $conn ) ) {
+                               // No last connection, probably due to all servers being too busy
                                $conn = new Database;
-                       }
-                       if ( $this->mFailFunction ) {
-                               $conn->failFunction( $this->mFailFunction );
+                               if ( $this->mFailFunction ) {
+                                       $conn->failFunction( $this->mFailFunction );
+                                       $conn->reportConnectionError( $this->mLastError );
+                               } else {
+                                       // If all servers were busy, mLastError will contain something sensible
+                                       wfEmergencyAbort( $conn, $this->mLastError );
+                               }
                        } else {
-                               $conn->failFunction( 'wfEmergencyAbort' );
+                               if ( $this->mFailFunction ) {
+                                       $conn->failFunction( $this->mFailFunction );
+                               } else {
+                                       $conn->failFunction( false );
+                               }
+                               $conn->reportConnectionError( "{$this->mLastError} ({$conn->mServer})" );
                        }
-                       $conn->reportConnectionError();
                        $reporting = false;
                }
                wfProfileOut( $fname );
        }
-       
+
        function getWriterIndex()
        {
                return 0;
@@ -467,7 +502,7 @@ class LoadBalancer {
        function saveMasterPos() {
                global $wgSessionStarted;
                if ( $wgSessionStarted && count( $this->mServers ) > 1 ) {
-                       # If this entire request was served from a slave without opening a connection to the 
+                       # If this entire request was served from a slave without opening a connection to the
                        # master (however unlikely that may be), then we can fetch the position from the slave.
                        if ( empty( $this->mConnections[0] ) ) {
                                $conn =& $this->getConnection( DB_SLAVE );
@@ -553,17 +588,19 @@ class LoadBalancer {
                }
                return array( $host, $maxLag );
        }
-       
+
        /**
         * Get lag time for each DB
         * Results are cached for a short time in memcached
         */
        function getLagTimes() {
+               global $wgDBname;
+               
                $expiry = 5;
                $requestRate = 10;
 
                global $wgMemc;
-               $times = $wgMemc->get( 'lag_times' );
+               $times = $wgMemc->get( "$wgDBname:lag_times" );
                if ( $times ) {
                        # Randomly recache with probability rising over $expiry
                        $elapsed = time() - $times['timestamp'];
@@ -585,7 +622,7 @@ class LoadBalancer {
 
                # Add a timestamp key so we know when it was cached
                $times['timestamp'] = time();
-               $wgMemc->set( 'lag_times', $times, $expiry );
+               $wgMemc->set( "$wgDBname:lag_times", $times, $expiry );
 
                # But don't give the timestamp to the caller
                unset($times['timestamp']);