Make DB handles inherit configured read-only mode
[lhc/web/wiklou.git] / includes / db / loadbalancer / LoadBalancer.php
index 3fcd349..bc9465b 100644 (file)
@@ -55,19 +55,26 @@ class LoadBalancer {
        /** @var bool|DBMasterPos False if not set */
        private $mWaitForPos;
        /** @var bool Whether the generic reader fell back to a lagged slave */
-       private $mLaggedSlaveMode;
+       private $laggedSlaveMode = false;
+       /** @var bool Whether the generic reader fell back to a lagged slave */
+       private $slavesDownMode = false;
        /** @var string The last DB selection or connection error */
        private $mLastError = 'Unknown error';
+       /** @var string|bool Reason the LB is read-only or false if not */
+       private $readOnlyReason = false;
        /** @var integer Total connections opened */
        private $connsOpened = 0;
 
        /** @var integer Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
+       /** @var integer Default 'max lag' when unspecified */
+       const MAX_LAG = 30;
 
        /**
         * @param array $params Array with keys:
-        *   servers           Required. Array of server info structures.
-        *   loadMonitor       Name of a class used to fetch server lag and load.
+        *  - servers : Required. Array of server info structures.
+        *  - loadMonitor : Name of a class used to fetch server lag and load.
+        *  - readOnlyReason : Reason the master DB is read-only if so [optional]
         * @throws MWException
         */
        public function __construct( array $params ) {
@@ -85,10 +92,13 @@ class LoadBalancer {
                        'foreignFree' => array() );
                $this->mLoads = array();
                $this->mWaitForPos = false;
-               $this->mLaggedSlaveMode = false;
                $this->mErrorConnection = false;
                $this->mAllowLagged = false;
 
+               if ( isset( $params['readOnlyReason'] ) && is_string( $params['readOnlyReason'] ) ) {
+                       $this->readOnlyReason = $params['readOnlyReason'];
+               }
+
                if ( isset( $params['loadMonitor'] ) ) {
                        $this->mLoadMonitorClass = $params['loadMonitor'];
                } else {
@@ -152,10 +162,10 @@ class LoadBalancer {
        /**
         * @param array $loads
         * @param bool|string $wiki Wiki to get non-lagged for
-        * @param float $maxLag Restrict the maximum allowed lag to this many seconds
+        * @param int $maxLag Restrict the maximum allowed lag to this many seconds
         * @return bool|int|string
         */
-       private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = INF ) {
+       private function getRandomNonLagged( array $loads, $wiki = false, $maxLag = self::MAX_LAG ) {
                $lags = $this->getLagTimes( $wiki );
 
                # Unset excessively lagged servers
@@ -327,7 +337,7 @@ class LoadBalancer {
                                $this->mReadIndex = $i;
                                # Record if the generic reader index is in "lagged slave" mode
                                if ( $laggedSlaveMode ) {
-                                       $this->mLaggedSlaveMode = true;
+                                       $this->laggedSlaveMode = true;
                                }
                        }
                        $serverName = $this->getServerName( $i );
@@ -350,7 +360,7 @@ class LoadBalancer {
                if ( $i > 0 ) {
                        if ( !$this->doWait( $i ) ) {
                                $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
-                               $this->mLaggedSlaveMode = true;
+                               $this->laggedSlaveMode = true;
                        }
                }
        }
@@ -546,6 +556,11 @@ class LoadBalancer {
                        $trxProf->recordConnection( $host, $dbname, $masterOnly );
                }
 
+               if ( $masterOnly ) {
+                       # Make master-requested DB handles inherit any read-only mode setting
+                       $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki ) );
+               }
+
                return $conn;
        }
 
@@ -1131,13 +1146,56 @@ class LoadBalancer {
        }
 
        /**
+        * @note This method will trigger a DB connection if not yet done
+        *
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
         * @return bool Whether the generic connection for reads is highly "lagged"
         */
-       public function getLaggedSlaveMode() {
-               # Get a generic reader connection
-               $this->getConnection( DB_SLAVE );
+       public function getLaggedSlaveMode( $wiki = false ) {
+               // No-op if there is only one DB (also avoids recursion)
+               if ( !$this->laggedSlaveMode && $this->getServerCount() > 1 ) {
+                       try {
+                               // See if laggedSlaveMode gets set
+                               $this->getConnection( DB_SLAVE, false, $wiki );
+                       } catch ( DBConnectionError $e ) {
+                               // Avoid expensive re-connect attempts and failures
+                               $this->slavesDownMode = true;
+                               $this->laggedSlaveMode = true;
+                       }
+               }
+
+               return $this->laggedSlaveMode;
+       }
+
+       /**
+        * @note This method will never cause a new DB connection
+        * @return bool Whether any generic connection used for reads was highly "lagged"
+        * @since 1.27
+        */
+       public function laggedSlaveUsed() {
+               return $this->laggedSlaveMode;
+       }
+
+       /**
+        * @note This method may trigger a DB connection if not yet done
+        * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @return string|bool Reason the master is read-only or false if it is not
+        * @since 1.27
+        */
+       public function getReadOnlyReason( $wiki = false ) {
+               if ( $this->readOnlyReason !== false ) {
+                       return $this->readOnlyReason;
+               } elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
+                       if ( $this->slavesDownMode ) {
+                               return 'The database has been automatically locked ' .
+                                       'until the slave database servers become available';
+                       } else {
+                               return 'The database has been automatically locked ' .
+                                       'while the slave database servers catch up to the master.';
+                       }
+               }
 
-               return $this->mLaggedSlaveMode;
+               return false;
        }
 
        /**
@@ -1221,12 +1279,14 @@ class LoadBalancer {
        }
 
        /**
-        * Get lag time for each server
+        * Get an estimate of replication lag (in seconds) for each server
         *
         * Results are cached for a short time in memcached/process cache
         *
+        * Values may be "false" if replication is too broken to estimate
+        *
         * @param string|bool $wiki
-        * @return int[] Map of (server index => seconds)
+        * @return int[] Map of (server index => float|int|bool)
         */
        public function getLagTimes( $wiki = false ) {
                if ( $this->getServerCount() <= 1 ) {