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
This commit is contained in:
parent
e67ca3a31c
commit
ceb7d61ee7
3 changed files with 427 additions and 91 deletions
|
|
@ -70,6 +70,9 @@ abstract class DatabaseMysqlBase extends Database {
|
||||||
/** @var stdClass|null */
|
/** @var stdClass|null */
|
||||||
private $replicationInfoRow = null;
|
private $replicationInfoRow = null;
|
||||||
|
|
||||||
|
// Cache getServerId() for 24 hours
|
||||||
|
const SERVER_ID_CACHE_TTL = 86400;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional $params include:
|
* Additional $params include:
|
||||||
* - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
|
* - 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)
|
// Wait on the GTID set (MariaDB only)
|
||||||
$gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
|
$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 {
|
} else {
|
||||||
// Wait on the binlog coordinates
|
// Wait on the binlog coordinates
|
||||||
$encFile = $this->addQuotes( $pos->getLogFile() );
|
$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)" );
|
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
|
||||||
}
|
}
|
||||||
|
|
||||||
$row = $res ? $this->fetchRow( $res ) : false;
|
$row = $res ? $this->fetchRow( $res ) : false;
|
||||||
if ( !$row ) {
|
if ( !$row ) {
|
||||||
throw new DBExpectedError( $this,
|
throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
|
||||||
"MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
|
// 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
|
* @return MySQLMasterPos|bool
|
||||||
*/
|
*/
|
||||||
public function getReplicaPos() {
|
public function getReplicaPos() {
|
||||||
$now = microtime( true );
|
$now = microtime( true ); // as-of-time *before* fetching GTID variables
|
||||||
|
|
||||||
if ( $this->useGTIDs ) {
|
if ( $this->useGTIDs() ) {
|
||||||
$res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ );
|
// Try to use GTIDs, fallbacking to binlog positions if not possible
|
||||||
$gtidRow = $this->fetchObject( $res );
|
$data = $this->getServerGTIDs( __METHOD__ );
|
||||||
if ( $gtidRow && strlen( $gtidRow->Value ) ) {
|
// Use gtid_current_pos for MariaDB and gtid_executed for MySQL
|
||||||
return new MySQLMasterPos( $gtidRow->Value, $now );
|
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__ );
|
$data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
|
||||||
$row = $this->fetchObject( $res );
|
if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
|
||||||
if ( $row && strlen( $row->Relay_Master_Log_File ) ) {
|
|
||||||
return new MySQLMasterPos(
|
return new MySQLMasterPos(
|
||||||
"{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}",
|
"{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
|
||||||
$now
|
$now
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -973,23 +983,97 @@ abstract class DatabaseMysqlBase extends Database {
|
||||||
* @return MySQLMasterPos|bool
|
* @return MySQLMasterPos|bool
|
||||||
*/
|
*/
|
||||||
public function getMasterPos() {
|
public function getMasterPos() {
|
||||||
$now = microtime( true );
|
$now = microtime( true ); // as-of-time *before* fetching GTID variables
|
||||||
|
|
||||||
if ( $this->useGTIDs ) {
|
$pos = false;
|
||||||
$res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ );
|
if ( $this->useGTIDs() ) {
|
||||||
$gtidRow = $this->fetchObject( $res );
|
// Try to use GTIDs, fallbacking to binlog positions if not possible
|
||||||
if ( $gtidRow && strlen( $gtidRow->Value ) ) {
|
$data = $this->getServerGTIDs( __METHOD__ );
|
||||||
return new MySQLMasterPos( $gtidRow->Value, $now );
|
// 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'] );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
|
if ( !$pos ) {
|
||||||
$row = $this->fetchObject( $res );
|
$data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
|
||||||
if ( $row && strlen( $row->File ) ) {
|
if ( $data && strlen( $data['File'] ) ) {
|
||||||
return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now );
|
$pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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 $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() {
|
public function serverIsReadOnly() {
|
||||||
|
|
@ -1434,6 +1518,12 @@ abstract class DatabaseMysqlBase extends Database {
|
||||||
return 'CAST( ' . $field . ' AS SIGNED )';
|
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' );
|
class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,36 @@ use UnexpectedValueException;
|
||||||
* - Binlog-based usage assumes single-source replication and non-hierarchical replication.
|
* - Binlog-based usage assumes single-source replication and non-hierarchical replication.
|
||||||
* - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
|
* - 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).
|
* 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 {
|
class MySQLMasterPos implements DBMasterPos {
|
||||||
/** @var string|null Binlog file base name */
|
/** @var int One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA) */
|
||||||
public $binlog;
|
private $style;
|
||||||
/** @var int[]|null Binglog file position tuple */
|
/** @var string|null Base name of all Binary Log files */
|
||||||
public $pos;
|
private $binLog;
|
||||||
/** @var string[] GTID list */
|
/** @var int[]|null Binary Log position tuple (index number, event number) */
|
||||||
public $gtids = [];
|
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 */
|
/** @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, <binlog file>/<integer>)
|
* @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
|
||||||
|
|
@ -38,18 +58,38 @@ class MySQLMasterPos implements DBMasterPos {
|
||||||
protected function init( $position, $asOfTime ) {
|
protected function init( $position, $asOfTime ) {
|
||||||
$m = [];
|
$m = [];
|
||||||
if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
|
if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
|
||||||
$this->binlog = $m[1]; // ideally something like host name
|
$this->binLog = $m[1]; // ideally something like host name
|
||||||
$this->pos = [ (int)$m[2], (int)$m[3] ];
|
$this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ];
|
||||||
|
$this->style = self::BINARY_LOG;
|
||||||
} else {
|
} else {
|
||||||
$gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
|
$gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
|
||||||
foreach ( $gtids as $gtid ) {
|
foreach ( $gtids as $gtid ) {
|
||||||
if ( !self::parseGTID( $gtid ) ) {
|
$components = self::parseGTID( $gtid );
|
||||||
|
if ( !$components ) {
|
||||||
throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
|
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 ) {
|
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
|
// Prefer GTID comparisons, which work with multi-tier replication
|
||||||
$thisPosByDomain = $this->getGtidCoordinates();
|
$thisPosByDomain = $this->getActiveGtidCoordinates();
|
||||||
$thatPosByDomain = $pos->getGtidCoordinates();
|
$thatPosByDomain = $pos->getActiveGtidCoordinates();
|
||||||
if ( $thisPosByDomain && $thatPosByDomain ) {
|
if ( $thisPosByDomain && $thatPosByDomain ) {
|
||||||
$comparisons = [];
|
$comparisons = [];
|
||||||
// Check that this has positions reaching those in $pos for all domains in common
|
// 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
|
// Prefer GTID comparisons, which work with multi-tier replication
|
||||||
$thisPosDomains = array_keys( $this->getGtidCoordinates() );
|
$thisPosDomains = array_keys( $this->getActiveGtidCoordinates() );
|
||||||
$thatPosDomains = array_keys( $pos->getGtidCoordinates() );
|
$thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() );
|
||||||
if ( $thisPosDomains && $thatPosDomains ) {
|
if ( $thisPosDomains && $thatPosDomains ) {
|
||||||
// Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
|
// 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
|
// 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 getLogFile() {
|
public function getLogName() {
|
||||||
return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}";
|
return $this->gtids ? null : $this->binLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string[]
|
* @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->logPos[self::CORD_INDEX]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[] Map of (server_uuid/gtid_domain_id => GTID)
|
||||||
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public function getGTIDs() {
|
public function getGTIDs() {
|
||||||
return $this->gtids;
|
return $this->gtids;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
|
* @param int|null $id @@gtid_domain_id of the active replication stream
|
||||||
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public function __toString() {
|
public function setActiveDomain( $id ) {
|
||||||
return $this->gtids
|
$this->activeDomain = (int)$id;
|
||||||
? implode( ',', $this->gtids )
|
}
|
||||||
: $this->getLogFile() . "/{$this->pos[1]}";
|
|
||||||
|
/**
|
||||||
|
* @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 $pos
|
||||||
* @param MySQLMasterPos $refPos
|
* @param MySQLMasterPos $refPos
|
||||||
* @return string[] List of GTIDs from $pos that have domains in $refPos
|
* @return string[] List of GTIDs from $pos that have domains in $refPos
|
||||||
|
* @since 1.31
|
||||||
*/
|
*/
|
||||||
public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
|
public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
|
||||||
$gtidsCommon = [];
|
return array_values(
|
||||||
|
array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() )
|
||||||
$relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused)
|
);
|
||||||
foreach ( $pos->gtids as $gtid ) {
|
|
||||||
list( $domain ) = self::parseGTID( $gtid );
|
|
||||||
if ( isset( $relevantDomains[$domain] ) ) {
|
|
||||||
$gtidsCommon[] = $gtid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $gtidsCommon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://mariadb.com/kb/en/mariadb/gtid
|
* @see https://mariadb.com/kb/en/mariadb/gtid
|
||||||
* @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
|
* @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 = [];
|
$gtidInfos = [];
|
||||||
foreach ( $this->gtids as $gtid ) {
|
|
||||||
list( $domain, $pos ) = self::parseGTID( $gtid );
|
foreach ( $this->gtids as $domain => $gtid ) {
|
||||||
$gtidInfos[$domain] = $pos;
|
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;
|
return $gtidInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $gtid
|
* @param string $id GTID
|
||||||
* @return array|null [domain, integer position] or null
|
* @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 = [];
|
$m = [];
|
||||||
if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
|
if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
|
||||||
// MariaDB style: <domain>-<server id>-<sequence number>
|
// MariaDB style: <domain>-<server id>-<sequence number>
|
||||||
return [ (int)$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+)$!', $gtid, $m ) ) {
|
} elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
|
||||||
// MySQL style: <UUID domain>:<sequence number>
|
// MySQL style: <server UUID>:<sequence number>-<sequence number>
|
||||||
return [ $m[1], (int)$m[2] ];
|
// 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;
|
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-master-status.html
|
||||||
* @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-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:<string>, pos:(<integer>, <integer>)) or false
|
||||||
*/
|
*/
|
||||||
protected function getBinlogCoordinates() {
|
protected function getBinlogCoordinates() {
|
||||||
return ( $this->binlog !== null && $this->pos !== null )
|
return ( $this->binLog !== null && $this->logPos !== null )
|
||||||
? [ 'binlog' => $this->binlog, 'pos' => $this->pos ]
|
? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
|
||||||
: false;
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,4 +299,13 @@ class MySQLMasterPos implements DBMasterPos {
|
||||||
|
|
||||||
$this->init( $data['position'], $data['asOfTime'] );
|
$this->init( $data['position'], $data['asOfTime'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string GTID set or <binary log file>/<position> (e.g db1034-bin.000976/843431247)
|
||||||
|
*/
|
||||||
|
public function __toString() {
|
||||||
|
return $this->gtids
|
||||||
|
? implode( ',', $this->gtids )
|
||||||
|
: $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,12 +137,15 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
|
||||||
$db->listViews( '' ) );
|
$db->listViews( '' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers Wikimedia\Rdbms\MySQLMasterPos
|
||||||
|
*/
|
||||||
public function testBinLogName() {
|
public function testBinLogName() {
|
||||||
$pos = new MySQLMasterPos( "db1052.2424/4643", 1 );
|
$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( "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
|
// MySQL GTID style
|
||||||
[
|
[
|
||||||
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:23', $now ),
|
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-23', $now ),
|
||||||
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:24', $now ),
|
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-24', $now ),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
|
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:5-99', $now ),
|
||||||
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
|
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:99', $now ),
|
new MySQLMasterPos( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-99', $now ),
|
||||||
new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:100', $now ),
|
new MySQLMasterPos( '1E11FA47-71CA-11E1-9E33-C80AA9429562:1-100', $now ),
|
||||||
false,
|
false,
|
||||||
false
|
false
|
||||||
],
|
],
|
||||||
|
|
@ -328,17 +331,17 @@ class DatabaseMysqlBaseTest extends PHPUnit\Framework\TestCase {
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new MySQLMasterPos(
|
new MySQLMasterPos(
|
||||||
'2E11FA47-71CA-11E1-9E33-C80AA9429562:5,' .
|
'2E11FA47-71CA-11E1-9E33-C80AA9429562:1-5,' .
|
||||||
'3E11FA47-71CA-11E1-9E33-C80AA9429562:99,' .
|
'3E11FA47-71CA-11E1-9E33-C80AA9429562:20-99,' .
|
||||||
'7E11FA47-71CA-11E1-9E33-C80AA9429562:30',
|
'7E11FA47-71CA-11E1-9E33-C80AA9429562:1-30',
|
||||||
1
|
1
|
||||||
),
|
),
|
||||||
new MySQLMasterPos(
|
new MySQLMasterPos(
|
||||||
'1E11FA47-71CA-11E1-9E33-C80AA9429562:100,' .
|
'1E11FA47-71CA-11E1-9E33-C80AA9429562:30-100,' .
|
||||||
'3E11FA47-71CA-11E1-9E33-C80AA9429562:66',
|
'3E11FA47-71CA-11E1-9E33-C80AA9429562:30-66',
|
||||||
1
|
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
|
* @covers Wikimedia\Rdbms\MySQLMasterPos
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue