rdbms: add more error logging to DatabaseMysqlBase::masterPosWait
[lhc/web/wiklou.git] / includes / libs / rdbms / database / DatabaseMysqlBase.php
index a5220b9..5e06f28 100644 (file)
@@ -70,6 +70,9 @@ abstract class DatabaseMysqlBase extends Database {
        /** @var stdClass|null */
        private $replicationInfoRow = null;
 
+       // Cache getServerId() for 24 hours
+       const SERVER_ID_CACHE_TTL = 86400;
+
        /**
         * Additional $params include:
         *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
@@ -558,17 +561,24 @@ abstract class DatabaseMysqlBase extends Database {
         * Takes same arguments as Database::select()
         *
         * @param string|array $table
-        * @param string|array $vars
+        * @param string|array $var
         * @param string|array $conds
         * @param string $fname
         * @param string|array $options
+        * @param array $join_conds
         * @return bool|int
         */
-       public function estimateRowCount( $table, $vars = '*', $conds = '',
-               $fname = __METHOD__, $options = []
+       public function estimateRowCount( $table, $var = '*', $conds = '',
+               $fname = __METHOD__, $options = [], $join_conds = []
        ) {
+               $conds = $this->normalizeConditions( $conds, $fname );
+               $column = $this->extractSingleFieldFromList( $var );
+               if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+                       $conds[] = "$column IS NOT NULL";
+               }
+
                $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
                if ( $res === false ) {
                        return false;
                }
@@ -891,22 +901,34 @@ abstract class DatabaseMysqlBase extends Database {
                        $rpos = $this->getReplicaPos();
                        $gtidsWait = $rpos ? MySQLMasterPos::getCommonDomainGTIDs( $pos, $rpos ) : [];
                        if ( !$gtidsWait ) {
+                               $this->queryLogger->error(
+                                       "No GTIDs with the same domain between master ($pos) and replica ($rpos)",
+                                       $this->getLogContext( [
+                                               'method' => __METHOD__,
+                                       ] )
+                               );
+
                                return -1; // $pos is from the wrong cluster?
                        }
                        // 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
@@ -938,21 +960,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_current_pos for MariaDB and gtid_executed for MySQL
+                       foreach ( [ 'gtid_current_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
                        );
                }
@@ -966,23 +990,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_current_pos for MariaDB and gtid_executed for MySQL
+                       foreach ( [ 'gtid_current_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() {
@@ -1281,10 +1379,6 @@ abstract class DatabaseMysqlBase extends Database {
                return $this->lastErrno() == 1205;
        }
 
-       public function wasErrorReissuable() {
-               return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
-       }
-
        /**
         * Determines if the last failure was due to the database being read-only.
         *
@@ -1418,43 +1512,24 @@ abstract class DatabaseMysqlBase extends Database {
                return in_array( $name, $this->listViews( $prefix ) );
        }
 
+       protected function isTransactableQuery( $sql ) {
+               return parent::isTransactableQuery( $sql ) &&
+                       !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
+       }
+
        /**
-        * Allows for index remapping in queries where this is not consistent across DBMS
-        *
-        * @param string $index
+        * @param string $field Field or column to cast
         * @return string
         */
-       protected function indexName( $index ) {
-               /**
-                * When SQLite indexes were introduced in r45764, it was noted that
-                * SQLite requires index names to be unique within the whole database,
-                * not just within a schema. As discussed in CR r45819, to avoid the
-                * need for a schema change on existing installations, the indexes
-                * were implicitly mapped from the new names to the old names.
-                *
-                * This mapping can be removed if DB patches are introduced to alter
-                * the relevant tables in existing installations. Note that because
-                * this index mapping applies to table creation, even new installations
-                * of MySQL have the old names (except for installations created during
-                * a period where this mapping was inappropriately removed, see
-                * T154872).
-                */
-               $renamed = [
-                       'ar_usertext_timestamp' => 'usertext_timestamp',
-                       'un_user_id' => 'user_id',
-                       'un_user_ip' => 'user_ip',
-               ];
-
-               if ( isset( $renamed[$index] ) ) {
-                       return $renamed[$index];
-               } else {
-                       return $index;
-               }
+       public function buildIntegerCast( $field ) {
+               return 'CAST( ' . $field . ' AS SIGNED )';
        }
 
-       protected function isTransactableQuery( $sql ) {
-               return parent::isTransactableQuery( $sql ) &&
-                       !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
+       /*
+        * @return bool Whether GTID support is used (mockable for testing)
+        */
+       protected function useGTIDs() {
+               return $this->useGTIDs;
        }
 }