From: jenkins-bot Date: Thu, 12 Apr 2018 01:03:15 +0000 (+0000) Subject: Merge "rdbms: ignore inactive mysql GTIDs in replication position methods" X-Git-Tag: 1.31.0-rc.0~114 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=296fe3b9009ec86aed06be88f9f08c400a09d78b;hp=-c Merge "rdbms: ignore inactive mysql GTIDs in replication position methods" --- 296fe3b9009ec86aed06be88f9f08c400a09d78b diff --combined includes/libs/rdbms/database/DatabaseMysqlBase.php index 1624122b7f,dad6cf91b3..3e6190ce9f --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@@ -320,7 -320,7 +320,7 @@@ abstract class DatabaseMysqlBase extend abstract protected function mysqlFetchObject( $res ); /** - * @param ResultWrapper|resource $res + * @param IResultWrapper|resource $res * @return array|bool * @throws DBUnexpectedError */ @@@ -766,20 -766,18 +766,20 @@@ protected function getLagFromPtHeartbeat() { $options = $this->lagDetectionOptions; - $staleness = $this->trxLevel - ? microtime( true ) - $this->trxTimestamp() - : 0; - if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) { - // Avoid returning higher and higher lag value due to snapshot age - // given that the isolation level will typically be REPEATABLE-READ - $this->queryLogger->warning( - "Using cached lag value for {db_server} due to active transaction", - $this->getLogContext( [ 'method' => __METHOD__ ] ) - ); + $currentTrxInfo = $this->getRecordedTransactionLagStatus(); + if ( $currentTrxInfo ) { + // There is an active transaction and the initial lag was already queried + $staleness = microtime( true ) - $currentTrxInfo['since']; + if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) { + // Avoid returning higher and higher lag value due to snapshot age + // given that the isolation level will typically be REPEATABLE-READ + $this->queryLogger->warning( + "Using cached lag value for {db_server} due to active transaction", + $this->getLogContext( [ 'method' => __METHOD__, 'age' => $staleness ] ) + ); + } - return $this->getTransactionLagStatus()['lag']; + return $currentTrxInfo['lag']; } if ( isset( $options['conds'] ) ) { @@@ -933,18 -931,23 +933,23 @@@ } // 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 -979,23 +981,23 @@@ * @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 -1009,97 +1011,97 @@@ * @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() { @@@ -1333,26 -1412,6 +1414,26 @@@ return $errno == 2013 || $errno == 2006; } + protected function wasKnownStatementRollbackError() { + $errno = $this->lastErrno(); + + if ( $errno === 1205 ) { // lock wait timeout + // Note that this is uncached to avoid stale values of SET is used + $row = $this->selectRow( + false, + [ 'innodb_rollback_on_timeout' => '@@innodb_rollback_on_timeout' ], + [], + __METHOD__ + ); + // https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html + // https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html + return $row->innodb_rollback_on_timeout ? false : true; + } + + // See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html + return in_array( $errno, [ 1022, 1216, 1217, 1137 ], true ); + } + /** * @param string $oldName * @param string $newName @@@ -1485,6 -1544,12 +1566,12 @@@ return 'CAST( ' . $field . ' AS SIGNED )'; } + /* + * @return bool Whether GTID support is used (mockable for testing) + */ + protected function useGTIDs() { + return $this->useGTIDs; + } } class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' ); diff --combined tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php index 378935c79a,b74a372fba..93192d01fb --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php @@@ -29,7 -29,6 +29,7 @@@ use Wikimedia\TestingAccessWrapper class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { use MediaWikiCoversValidator; + use PHPUnit4And6Compat; /** * @dataProvider provideDiapers @@@ -138,12 -137,15 +138,15 @@@ $db->listViews( '' ) ); } + /** + * @covers Wikimedia\Rdbms\MySQLMasterPos + */ public function testBinLogName() { $pos = new MySQLMasterPos( "db1052.2424/4643", 1 ); - $this->assertEquals( "db1052", $pos->binlog ); + $this->assertEquals( "db1052", $pos->getLogName() ); $this->assertEquals( "db1052.2424", $pos->getLogFile() ); - $this->assertEquals( [ 2424, 4643 ], $pos->pos ); + $this->assertEquals( [ 2424, 4643 ], $pos->getLogPosition() ); } /** @@@ -198,20 -200,20 +201,20 @@@ ], // MySQL GTID style [ - new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:23', $now ), - new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:24', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ), true, false ], [ - new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ), - new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ), true, false ], [ - new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ), - new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ), + new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ), + new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ), false, false ], @@@ -329,17 -331,17 +332,17 @@@ ], [ new MySQLMasterPos( - '2E11FA47-71CA-11E1-9E33-C80AA9429562:5,' . - '3E11FA47-71CA-11E1-9E33-C80AA9429562:99,' . - '7E11FA47-71CA-11E1-9E33-C80AA9429562:30', + '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' . + '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' . + '7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30', 1 ), new MySQLMasterPos( - '1E11FA47-71CA-11E1-9E33-C80AA9429562:100,' . - '3E11FA47-71CA-11E1-9E33-C80AA9429562:66', + '1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' . + '3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66', 1 ), - [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:99' ] + [ '3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99' ] ] ]; } @@@ -398,6 -400,160 +401,160 @@@ ]; } + /** + * @dataProvider provideGtidData + * @covers Wikimedia\Rdbms\MySQLMasterPos + * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getReplicaPos + * @covers Wikimedia\Rdbms\DatabaseMysqlBase::getMasterPos + */ + public function testServerGtidTable( $gtable, $rBLtable, $mBLtable, $rGTIDs, $mGTIDs ) { + $db = $this->getMockBuilder( DatabaseMysqli::class ) + ->disableOriginalConstructor() + ->setMethods( [ + 'useGTIDs', + 'getServerGTIDs', + 'getServerRoleStatus', + 'getServerId', + 'getServerUUID' + ] ) + ->getMock(); + + $db->method( 'useGTIDs' )->willReturn( true ); + $db->method( 'getServerGTIDs' )->willReturn( $gtable ); + $db->method( 'getServerRoleStatus' )->willReturnCallback( + function ( $role ) use ( $rBLtable, $mBLtable ) { + if ( $role === 'SLAVE' ) { + return $rBLtable; + } elseif ( $role === 'MASTER' ) { + return $mBLtable; + } + + return null; + } + ); + $db->method( 'getServerId' )->willReturn( 1 ); + $db->method( 'getServerUUID' )->willReturn( '2E11FA47-71CA-11E1-9E33-C80AA9429562' ); + + if ( is_array( $rGTIDs ) ) { + $this->assertEquals( $rGTIDs, $db->getReplicaPos()->getGTIDs() ); + } else { + $this->assertEquals( false, $db->getReplicaPos() ); + } + if ( is_array( $mGTIDs ) ) { + $this->assertEquals( $mGTIDs, $db->getMasterPos()->getGTIDs() ); + } else { + $this->assertEquals( false, $db->getMasterPos() ); + } + } + + public static function provideGtidData() { + return [ + // MariaDB + [ + [ + 'gtid_domain_id' => 100, + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => null // master + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [ + 'File' => 'host.1600', + 'Position' => '77' + ], + [], + [ '100' => '100-13-77' ] + ], + [ + [ + 'gtid_domain_id' => 100, + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => '100-13-77' // replica + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], + [ '100' => '100-13-77' ], + [ '100' => '100-13-77' ] + ], + [ + [ + 'gtid_current_pos' => '100-13-77', + 'gtid_binlog_pos' => '100-13-77', + 'gtid_slave_pos' => '100-13-77' // replica + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], + [ '100' => '100-13-77' ], + [ '100' => '100-13-77' ] + ], + // MySQL + [ + [ + 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ], + // replica/master use same var + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-77' ], + ], + [ + [ + 'gtid_executed' => '2E11FA47-71CA-11E1-9E33-C80AA9429562:1-49,' . + '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ], + // replica/master use same var + [ '2E11FA47-71CA-11E1-9E33-C80AA9429562' + => '2E11FA47-71CA-11E1-9E33-C80AA9429562:51-77' ], + ], + [ + [ + 'gtid_executed' => null, // not enabled? + 'gtid_binlog_pos' => null + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [], // binlog fallback + false + ], + [ + [ + 'gtid_executed' => null, // not enabled? + 'gtid_binlog_pos' => null + ], + [], // no replication + [], // no replication + false, + false + ] + ]; + } + /** * @covers Wikimedia\Rdbms\MySQLMasterPos */