X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FDatabaseMysqlBase.php;h=5e06f28d64f63994c8dc13fe43828f33b41136f7;hp=a5220b9a2e7a3501f94f0a14b611e8a0406af6bf;hb=7d8ea236ca7b0c30dd4665cd84adb88fe29c4938;hpb=ca8cd77115d4369a3d08554e152acd08a83e964a diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index a5220b9a2e..5e06f28d64 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -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; } }