Merge "Introduce ApiMaxLagInfo hook"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / DatabaseMysqlBase.php
index 1624122..78c6d91 100644 (file)
@@ -96,12 +96,8 @@ abstract class DatabaseMysqlBase extends Database {
         * @param array $params
         */
        function __construct( array $params ) {
-               $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
-                       ? $params['lagDetectionMethod']
-                       : 'Seconds_Behind_Master';
-               $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
-                       ? $params['lagDetectionOptions']
-                       : [];
+               $this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
+               $this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
                $this->useGTIDs = !empty( $params['useGTIDs' ] );
                foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
                        $var = "ssl{$name}";
@@ -109,7 +105,7 @@ abstract class DatabaseMysqlBase extends Database {
                                $this->$var = $params[$var];
                        }
                }
-               $this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
+               $this->sqlMode = $params['sqlMode'] ?? '';
                $this->utf8Mode = !empty( $params['utf8Mode'] );
                $this->insertSelectIsSafe = isset( $params['insertSelectIsSafe'] )
                        ? (bool)$params['insertSelectIsSafe'] : null;
@@ -152,9 +148,7 @@ abstract class DatabaseMysqlBase extends Database {
 
                # Always log connection errors
                if ( !$this->conn ) {
-                       if ( !$error ) {
-                               $error = $this->lastError();
-                       }
+                       $error = $error ?: $this->lastError();
                        $this->connLogger->error(
                                "Error connecting to {db_server}: {error}",
                                $this->getLogContext( [
@@ -166,7 +160,7 @@ abstract class DatabaseMysqlBase extends Database {
                                "Server: $server, User: $user, Password: " .
                                substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
 
-                       $this->reportConnectionError( $error );
+                       throw new DBConnectionError( $this, $error );
                }
 
                if ( strlen( $dbName ) ) {
@@ -174,22 +168,29 @@ abstract class DatabaseMysqlBase extends Database {
                        $success = $this->selectDB( $dbName );
                        Wikimedia\restoreWarnings();
                        if ( !$success ) {
+                               $error = $this->lastError();
                                $this->queryLogger->error(
-                                       "Error selecting database {db_name} on server {db_server}",
+                                       "Error selecting database {db_name} on server {db_server}: {error}",
                                        $this->getLogContext( [
                                                'method' => __METHOD__,
+                                               'error' => $error,
                                        ] )
                                );
-                               $this->queryLogger->debug(
-                                       "Error selecting database $dbName on server {$this->server}" );
-
-                               $this->reportConnectionError( "Error selecting database $dbName" );
+                               throw new DBConnectionError( $this, "Error selecting database $dbName: $error" );
                        }
                }
 
                // Tell the server what we're communicating with
                if ( !$this->connectInitCharset() ) {
-                       $this->reportConnectionError( "Error setting character set" );
+                       $error = $this->lastError();
+                       $this->queryLogger->error(
+                               "Error setting character set: {error}",
+                               $this->getLogContext( [
+                                       'method' => __METHOD__,
+                                       'error' => $this->lastError(),
+                               ] )
+                       );
+                       throw new DBConnectionError( $this, "Error setting character set: $error" );
                }
 
                // Abstract over any insane MySQL defaults
@@ -212,14 +213,15 @@ abstract class DatabaseMysqlBase extends Database {
                        // Use doQuery() to avoid opening implicit transactions (DBO_TRX)
                        $success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
                        if ( !$success ) {
+                               $error = $this->lastError();
                                $this->queryLogger->error(
-                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
+                                       'Error setting MySQL variables on server {db_server}: {error}',
                                        $this->getLogContext( [
                                                'method' => __METHOD__,
+                                               'error' => $error,
                                        ] )
                                );
-                               $this->reportConnectionError(
-                                       'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
+                               throw new DBConnectionError( $this, "Error setting MySQL variables: $error" );
                        }
                }
 
@@ -933,18 +935,23 @@ abstract class DatabaseMysqlBase extends Database {
                        }
                        // Wait on the GTID set (MariaDB only)
                        $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
-                       $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+                       if ( strpos( $gtidArg, ':' ) !== false ) {
+                               // MySQL GTIDs, e.g "source_id:transaction_id"
+                               $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
+                       } else {
+                               // MariaDB GTIDs, e.g."domain:server:sequence"
+                               $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+                       }
                } else {
                        // Wait on the binlog coordinates
                        $encFile = $this->addQuotes( $pos->getLogFile() );
-                       $encPos = intval( $pos->pos[1] );
+                       $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
                        $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
                }
 
                $row = $res ? $this->fetchRow( $res ) : false;
                if ( !$row ) {
-                       throw new DBExpectedError( $this,
-                               "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
+                       throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
                }
 
                // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
@@ -976,21 +983,23 @@ abstract class DatabaseMysqlBase extends Database {
         * @return MySQLMasterPos|bool
         */
        public function getReplicaPos() {
-               $now = microtime( true );
-
-               if ( $this->useGTIDs ) {
-                       $res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ );
-                       $gtidRow = $this->fetchObject( $res );
-                       if ( $gtidRow && strlen( $gtidRow->Value ) ) {
-                               return new MySQLMasterPos( $gtidRow->Value, $now );
+               $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+               if ( $this->useGTIDs() ) {
+                       // Try to use GTIDs, fallbacking to binlog positions if not possible
+                       $data = $this->getServerGTIDs( __METHOD__ );
+                       // Use gtid_slave_pos for MariaDB and gtid_executed for MySQL
+                       foreach ( [ 'gtid_slave_pos', 'gtid_executed' ] as $name ) {
+                               if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+                                       return new MySQLMasterPos( $data[$name], $now );
+                               }
                        }
                }
 
-               $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
-               $row = $this->fetchObject( $res );
-               if ( $row && strlen( $row->Relay_Master_Log_File ) ) {
+               $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
+               if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
                        return new MySQLMasterPos(
-                               "{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}",
+                               "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
                                $now
                        );
                }
@@ -1004,23 +1013,97 @@ abstract class DatabaseMysqlBase extends Database {
         * @return MySQLMasterPos|bool
         */
        public function getMasterPos() {
-               $now = microtime( true );
+               $now = microtime( true ); // as-of-time *before* fetching GTID variables
+
+               $pos = false;
+               if ( $this->useGTIDs() ) {
+                       // Try to use GTIDs, fallbacking to binlog positions if not possible
+                       $data = $this->getServerGTIDs( __METHOD__ );
+                       // Use gtid_binlog_pos for MariaDB and gtid_executed for MySQL
+                       foreach ( [ 'gtid_binlog_pos', 'gtid_executed' ] as $name ) {
+                               if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+                                       $pos = new MySQLMasterPos( $data[$name], $now );
+                                       break;
+                               }
+                       }
+                       // Filter domains that are inactive or not relevant to the session
+                       if ( $pos ) {
+                               $pos->setActiveOriginServerId( $this->getServerId() );
+                               $pos->setActiveOriginServerUUID( $this->getServerUUID() );
+                               if ( isset( $data['gtid_domain_id'] ) ) {
+                                       $pos->setActiveDomain( $data['gtid_domain_id'] );
+                               }
+                       }
+               }
 
-               if ( $this->useGTIDs ) {
-                       $res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ );
-                       $gtidRow = $this->fetchObject( $res );
-                       if ( $gtidRow && strlen( $gtidRow->Value ) ) {
-                               return new MySQLMasterPos( $gtidRow->Value, $now );
+               if ( !$pos ) {
+                       $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
+                       if ( $data && strlen( $data['File'] ) ) {
+                               $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
                        }
                }
 
-               $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
-               $row = $this->fetchObject( $res );
-               if ( $row && strlen( $row->File ) ) {
-                       return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now );
+               return $pos;
+       }
+
+       /**
+        * @return int
+        * @throws DBQueryError If the variable doesn't exist for some reason
+        */
+       protected function getServerId() {
+               return $this->srvCache->getWithSetCallback(
+                       $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ),
+                       self::SERVER_ID_CACHE_TTL,
+                       function () {
+                               $res = $this->query( "SELECT @@server_id AS id", __METHOD__ );
+                               return intval( $this->fetchObject( $res )->id );
+                       }
+               );
+       }
+
+       /**
+        * @return string|null
+        */
+       protected function getServerUUID() {
+               return $this->srvCache->getWithSetCallback(
+                       $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ),
+                       self::SERVER_ID_CACHE_TTL,
+                       function () {
+                               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" );
+                               $row = $this->fetchObject( $res );
+
+                               return $row ? $row->Value : null;
+                       }
+               );
+       }
+
+       /**
+        * @param string $fname
+        * @return string[]
+        */
+       protected function getServerGTIDs( $fname = __METHOD__ ) {
+               $map = [];
+               // Get global-only variables like gtid_executed
+               $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname );
+               foreach ( $res as $row ) {
+                       $map[$row->Variable_name] = $row->Value;
+               }
+               // Get session-specific (e.g. gtid_domain_id since that is were writes will log)
+               $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname );
+               foreach ( $res as $row ) {
+                       $map[$row->Variable_name] = $row->Value;
                }
 
-               return false;
+               return $map;
+       }
+
+       /**
+        * @param string $role One of "MASTER"/"SLAVE"
+        * @param string $fname
+        * @return string[] Latest available server status row
+        */
+       protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
+               return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: [];
        }
 
        public function serverIsReadOnly() {
@@ -1485,6 +1568,15 @@ abstract class DatabaseMysqlBase extends Database {
                return 'CAST( ' . $field . ' AS SIGNED )';
        }
 
+       /*
+        * @return bool Whether GTID support is used (mockable for testing)
+        */
+       protected function useGTIDs() {
+               return $this->useGTIDs;
+       }
 }
 
+/**
+ * @deprecated since 1.29
+ */
 class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );