Automatically detect READ_ONLY mode for MySQL/MariaDB
authorAaron Schulz <aschulz@wikimedia.org>
Fri, 22 Jul 2016 05:15:30 +0000 (22:15 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Sat, 30 Jul 2016 02:32:06 +0000 (02:32 +0000)
This avoids having users think they can make edits when an
exception will just be thrown when they try to save. Likewise
for other write actions.

Bug: T24923
Change-Id: I49c4057b672875ec6f34681a5668a509cec05677

includes/db/DBConnRef.php
includes/db/Database.php
includes/db/DatabaseMysqlBase.php
includes/db/IDatabase.php
includes/db/loadbalancer/LoadBalancer.php

index 1893c73..53862b9 100644 (file)
@@ -417,6 +417,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function serverIsReadOnly() {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        public function onTransactionResolution( callable $callback ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
index 2e3e225..3dc6e92 100644 (file)
@@ -2456,6 +2456,10 @@ abstract class DatabaseBase implements IDatabase {
                return false;
        }
 
+       public function serverIsReadOnly() {
+               return false;
+       }
+
        final public function onTransactionResolution( callable $callback ) {
                if ( !$this->mTrxLevel ) {
                        throw new DBUnexpectedError( $this, "No transaction is active." );
index 02a8d30..a6f8c31 100644 (file)
@@ -885,6 +885,13 @@ abstract class DatabaseMysqlBase extends Database {
                }
        }
 
+       public function serverIsReadOnly() {
+               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
+               $row = $this->fetchObject( $res );
+
+               return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
+       }
+
        /**
         * @param string $index
         * @return string
index aa2a980..41b131f 100644 (file)
@@ -1220,6 +1220,12 @@ interface IDatabase {
         */
        public function getMasterPos();
 
+       /**
+        * @return bool Whether the DB is marked as read-only server-side
+        * @since 1.28
+        */
+       public function serverIsReadOnly();
+
        /**
         * Run a callback as soon as the current transaction commits or rolls back.
         * An error is thrown if no transaction is pending. Queries in the function will run in
index a67eac1..2543958 100644 (file)
@@ -49,6 +49,8 @@ class LoadBalancer {
        private $mLoadMonitor;
        /** @var BagOStuff */
        private $srvCache;
+       /** @var WANObjectCache */
+       private $wanCache;
 
        /** @var bool|DatabaseBase Database connection that caused a problem */
        private $mErrorConnection;
@@ -76,6 +78,8 @@ class LoadBalancer {
        const MAX_LAG = 10;
        /** @var integer Max time to wait for a slave to catch up (e.g. ChronologyProtector) */
        const POS_WAIT_TIMEOUT = 10;
+       /** @var integer Seconds to cache master server read-only status */
+       const TTL_CACHE_READONLY = 5;
 
        /**
         * @var boolean
@@ -135,6 +139,7 @@ class LoadBalancer {
                }
 
                $this->srvCache = ObjectCache::getLocalServerInstance();
+               $this->wanCache = ObjectCache::getMainWANInstance();
 
                if ( isset( $params['trxProfiler'] ) ) {
                        $this->trxProfiler = $params['trxProfiler'];
@@ -578,7 +583,7 @@ class LoadBalancer {
 
                if ( $masterOnly ) {
                        # Make master-requested DB handles inherit any read-only mode setting
-                       $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki ) );
+                       $conn->setLBInfo( 'readOnlyReason', $this->getReadOnlyReason( $wiki, $conn ) );
                }
 
                return $conn;
@@ -1274,10 +1279,11 @@ class LoadBalancer {
        /**
         * @note This method may trigger a DB connection if not yet done
         * @param string|bool $wiki Wiki ID, or false for the current wiki
+        * @param DatabaseBase|null DB master connection; used to avoid loops [optional]
         * @return string|bool Reason the master is read-only or false if it is not
         * @since 1.27
         */
-       public function getReadOnlyReason( $wiki = false ) {
+       public function getReadOnlyReason( $wiki = false, DatabaseBase $conn = null ) {
                if ( $this->readOnlyReason !== false ) {
                        return $this->readOnlyReason;
                } elseif ( $this->getLaggedSlaveMode( $wiki ) ) {
@@ -1288,11 +1294,37 @@ class LoadBalancer {
                                return 'The database has been automatically locked ' .
                                        'while the slave database servers catch up to the master.';
                        }
+               } elseif ( $this->masterRunningReadOnly( $wiki, $conn ) ) {
+                       return 'The database master is running in read-only mode.';
                }
 
                return false;
        }
 
+       /**
+        * @param string $wiki Wiki ID, or false for the current wiki
+        * @param DatabaseBase|null DB master connectionl used to avoid loops [optional]
+        * @return bool
+        */
+       private function masterRunningReadOnly( $wiki, DatabaseBase $conn = null ) {
+               $cache = $this->wanCache;
+               $masterServer = $this->getServerName( $this->getWriterIndex() );
+
+               return (bool)$cache->getWithSetCallback(
+                       $cache->makeGlobalKey( __CLASS__, 'server-read-only', $masterServer ),
+                       self::TTL_CACHE_READONLY,
+                       function () use ( $wiki, $conn ) {
+                               try {
+                                       $dbw = $conn ?: $this->getConnection( DB_MASTER, [], $wiki );
+                                       return (int)$dbw->serverIsReadOnly();
+                               } catch ( DBError $e ) {
+                                       return 0;
+                               }
+                       },
+                       [ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ]
+               );
+       }
+
        /**
         * Disables/enables lag checks
         * @param null|bool $mode