From ceb7d61ee7ef3edc6705abd41ec86b3afcd9c491 Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Wed, 7 Feb 2018 02:15:54 -0800 Subject: [PATCH] rdbms: make getMasterPos() ignore GTIDs outside of gtid_domain_id * Filter out GTIDs with a domain that is not the one binlog events would be written to if the Database handle was given write queries. Likewise for the MariaDB server_id component. * Also improve MySQL GTID support to better match that of MariaDB. This covers position retrieval, replication waiting, and ranges in GTIDs (which are almost always present). * Make some MySQLMasterPos variables private by making use of accesors instead. * Store the gtids array keyed by domain ID for convenience. * Clean up dynamic call to static method. Change-Id: Ic6ab517bc8f200c968ff892ade69ad1b9394ab21 --- .../libs/rdbms/database/DatabaseMysqlBase.php | 142 ++++++++++--- .../database/position/MySQLMasterPos.php | 196 +++++++++++++----- .../rdbms/database/DatabaseMysqlBaseTest.php | 180 ++++++++++++++-- 3 files changed, 427 insertions(+), 91 deletions(-) diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index c7147e4875..286d65881e 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). @@ -902,18 +905,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 @@ -945,21 +953,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 ); } @@ -973,23 +983,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() { @@ -1434,6 +1518,12 @@ 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; + } } class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' ); diff --git a/includes/libs/rdbms/database/position/MySQLMasterPos.php b/includes/libs/rdbms/database/position/MySQLMasterPos.php index cdcb79cde6..38f2bd6baf 100644 --- a/includes/libs/rdbms/database/position/MySQLMasterPos.php +++ b/includes/libs/rdbms/database/position/MySQLMasterPos.php @@ -12,16 +12,36 @@ use UnexpectedValueException; * - Binlog-based usage assumes single-source replication and non-hierarchical replication. * - GTID-based usage allows getting/syncing with multi-source replication. It is assumed * that GTID sets are complete (e.g. include all domains on the server). + * + * @see https://mariadb.com/kb/en/library/gtid/ + * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html */ class MySQLMasterPos implements DBMasterPos { - /** @var string|null Binlog file base name */ - public $binlog; - /** @var int[]|null Binglog file position tuple */ - public $pos; - /** @var string[] GTID list */ - public $gtids = []; + /** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */ + private $style; + /** @var string|null Base name of all Binary Log files */ + private $binLog; + /** @var int[]|null Binary Log position tuple (index number, event number) */ + private $logPos; + /** @var string[] Map of (server_uuid/gtid_domain_id => GTID) */ + private $gtids = []; + /** @var int|null Active GTID domain ID */ + private $activeDomain; + /** @var int|null ID of the server were DB writes originate */ + private $activeServerId; + /** @var string|null UUID of the server were DB writes originate */ + private $activeServerUUID; /** @var float UNIX timestamp */ - public $asOfTime = 0.0; + private $asOfTime = 0.0; + + const BINARY_LOG = 'binary-log'; + const GTID_MARIA = 'gtid-maria'; + const GTID_MYSQL = 'gtid-mysql'; + + /** @var int Key name of the binary log index number of a position tuple */ + const CORD_INDEX = 0; + /** @var int Key name of the binary log event number of a position tuple */ + const CORD_EVENT = 1; /** * @param string $position One of (comma separated GTID list, /) @@ -38,18 +58,38 @@ class MySQLMasterPos implements DBMasterPos { protected function init( $position, $asOfTime ) { $m = []; if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) { - $this->binlog = $m[1]; // ideally something like host name - $this->pos = [ (int)$m[2], (int)$m[3] ]; + $this->binLog = $m[1]; // ideally something like host name + $this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ]; + $this->style = self::BINARY_LOG; } else { $gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) ); foreach ( $gtids as $gtid ) { - if ( !self::parseGTID( $gtid ) ) { + $components = self::parseGTID( $gtid ); + if ( !$components ) { throw new InvalidArgumentException( "Invalid GTID '$gtid'." ); } - $this->gtids[] = $gtid; + + list( $domain, $pos ) = $components; + if ( isset( $this->gtids[$domain] ) ) { + // For MySQL, handle the case where some past issue caused a gap in the + // executed GTID set, e.g. [last_purged+1,N-1] and [N+1,N+2+K]. Ignore the + // gap by using the GTID with the highest ending sequence number. + list( , $otherPos ) = self::parseGTID( $this->gtids[$domain] ); + if ( $pos > $otherPos ) { + $this->gtids[$domain] = $gtid; + } + } else { + $this->gtids[$domain] = $gtid; + } + + if ( is_int( $domain ) ) { + $this->style = self::GTID_MARIA; // gtid_domain_id + } else { + $this->style = self::GTID_MYSQL; // server_uuid + } } if ( !$this->gtids ) { - throw new InvalidArgumentException( "Got empty GTID set." ); + throw new InvalidArgumentException( "GTID set cannot be empty." ); } } @@ -66,8 +106,8 @@ class MySQLMasterPos implements DBMasterPos { } // Prefer GTID comparisons, which work with multi-tier replication - $thisPosByDomain = $this->getGtidCoordinates(); - $thatPosByDomain = $pos->getGtidCoordinates(); + $thisPosByDomain = $this->getActiveGtidCoordinates(); + $thatPosByDomain = $pos->getActiveGtidCoordinates(); if ( $thisPosByDomain && $thatPosByDomain ) { $comparisons = []; // Check that this has positions reaching those in $pos for all domains in common @@ -100,8 +140,8 @@ class MySQLMasterPos implements DBMasterPos { } // Prefer GTID comparisons, which work with multi-tier replication - $thisPosDomains = array_keys( $this->getGtidCoordinates() ); - $thatPosDomains = array_keys( $pos->getGtidCoordinates() ); + $thisPosDomains = array_keys( $this->getActiveGtidCoordinates() ); + $thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() ); if ( $thisPosDomains && $thatPosDomains ) { // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot @@ -118,74 +158,119 @@ class MySQLMasterPos implements DBMasterPos { } /** - * @return string|null + * @return string|null Base name of binary log files + * @since 1.31 + */ + public function getLogName() { + return $this->gtids ? null : $this->binLog; + } + + /** + * @return int[]|null Tuple of (binary log file number, event number) + * @since 1.31 + */ + public function getLogPosition() { + return $this->gtids ? null : $this->logPos; + } + + /** + * @return string|null Name of the binary log file for this position + * @since 1.31 */ public function getLogFile() { - return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}"; + return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}"; } /** - * @return string[] + * @return string[] Map of (server_uuid/gtid_domain_id => GTID) + * @since 1.31 */ public function getGTIDs() { return $this->gtids; } /** - * @return string GTID set or / (e.g db1034-bin.000976/843431247) + * @param int|null $id @@gtid_domain_id of the active replication stream + * @since 1.31 */ - public function __toString() { - return $this->gtids - ? implode( ',', $this->gtids ) - : $this->getLogFile() . "/{$this->pos[1]}"; + public function setActiveDomain( $id ) { + $this->activeDomain = (int)$id; + } + + /** + * @param int|null $id @@server_id of the server were writes originate + * @since 1.31 + */ + public function setActiveOriginServerId( $id ) { + $this->activeServerId = (int)$id; + } + + /** + * @param string|null $id @@server_uuid of the server were writes originate + * @since 1.31 + */ + public function setActiveOriginServerUUID( $id ) { + $this->activeServerUUID = $id; } /** * @param MySQLMasterPos $pos * @param MySQLMasterPos $refPos * @return string[] List of GTIDs from $pos that have domains in $refPos + * @since 1.31 */ public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) { - $gtidsCommon = []; - - $relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused) - foreach ( $pos->gtids as $gtid ) { - list( $domain ) = self::parseGTID( $gtid ); - if ( isset( $relevantDomains[$domain] ) ) { - $gtidsCommon[] = $gtid; - } - } - - return $gtidsCommon; + return array_values( + array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() ) + ); } /** * @see https://mariadb.com/kb/en/mariadb/gtid * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html - * @return array Map of (domain => integer position); possibly empty + * @return array Map of (server_uuid/gtid_domain_id => integer position); possibly empty */ - protected function getGtidCoordinates() { + protected function getActiveGtidCoordinates() { $gtidInfos = []; - foreach ( $this->gtids as $gtid ) { - list( $domain, $pos ) = self::parseGTID( $gtid ); - $gtidInfos[$domain] = $pos; + + foreach ( $this->gtids as $domain => $gtid ) { + list( $domain, $pos, $server ) = self::parseGTID( $gtid ); + + $ignore = false; + // Filter out GTIDs from non-active replication domains + if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) { + $ignore |= ( $domain !== $this->activeDomain ); + } + // Likewise for GTIDs from non-active replication origin servers + if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) { + $ignore |= ( $server !== $this->activeServerId ); + } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) { + $ignore |= ( $server !== $this->activeServerUUID ); + } + + if ( !$ignore ) { + $gtidInfos[$domain] = $pos; + } } return $gtidInfos; } /** - * @param string $gtid - * @return array|null [domain, integer position] or null + * @param string $id GTID + * @return array|null [domain ID or server UUID, sequence number, server ID/UUID] or null */ - protected static function parseGTID( $gtid ) { + protected static function parseGTID( $id ) { $m = []; - if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) { + if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) { // MariaDB style: -- - return [ (int)$m[1], (int)$m[2] ]; - } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) { - // MySQL style: : - return [ $m[1], (int)$m[2] ]; + return [ (int)$m[1], (int)$m[3], (int)$m[2] ]; + } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) { + // MySQL style: :- + // Normally, the first number should reflect the point (gtid_purged) where older + // binary logs where purged to save space. When doing comparisons, it may as well + // be 1 in that case. Assume that this is generally the situation. + return [ $m[1], (int)$m[2], $m[1] ]; } return null; @@ -194,11 +279,11 @@ class MySQLMasterPos implements DBMasterPos { /** * @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html * @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html - * @return array|bool (binlog, (integer file number, integer position)) or false + * @return array|bool Map of (binlog:, pos:(, )) or false */ protected function getBinlogCoordinates() { - return ( $this->binlog !== null && $this->pos !== null ) - ? [ 'binlog' => $this->binlog, 'pos' => $this->pos ] + return ( $this->binLog !== null && $this->logPos !== null ) + ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ] : false; } @@ -214,4 +299,13 @@ class MySQLMasterPos implements DBMasterPos { $this->init( $data['position'], $data['asOfTime'] ); } + + /** + * @return string GTID set or / (e.g db1034-bin.000976/843431247) + */ + public function __toString() { + return $this->gtids + ? implode( ',', $this->gtids ) + : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}"; + } } diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php index 1eca89bb22..a4edbe7b70 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseMysqlBaseTest.php @@ -137,12 +137,15 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { $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() ); } /** @@ -197,20 +200,20 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { ], // 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 ], @@ -328,17 +331,17 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { ], [ 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' ] ] ]; } @@ -397,6 +400,155 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase { ]; } + /** + * @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 + ], + [], + [ + 'File' => 'host.1600', + 'Pos' => '77' + ], + [ '100' => '100-13-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? + ], + [ + 'Relay_Master_Log_File' => 'host.1600', + 'Exec_Master_Log_Pos' => '77' + ], + [], // only a replica + [], // binlog fallback + false + ], + [ + [ + 'gtid_executed' => null // not enabled? + ], + [], // no replication + [], // no replication + false, + false + ] + ]; + } + /** * @covers Wikimedia\Rdbms\MySQLMasterPos */ -- 2.20.1