2ee90684466c79ed2dbc12ddab360cc5fb6d27f1
3 namespace Wikimedia\Rdbms
;
5 use InvalidArgumentException
;
8 * DBMasterPos class for MySQL/MariaDB
10 * Note that master positions and sync logic here make some assumptions:
11 * - Binlog-based usage assumes single-source replication and non-hierarchical replication.
12 * - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
13 * that GTID sets are complete (e.g. include all domains on the server).
15 class MySQLMasterPos
implements DBMasterPos
{
16 /** @var string|null Binlog file base name */
18 /** @var int[]|null Binglog file position tuple */
20 /** @var string[] GTID list */
22 /** @var float UNIX timestamp */
23 public $asOfTime = 0.0;
26 * @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
27 * @param float $asOfTime UNIX timestamp
29 public function __construct( $position, $asOfTime ) {
31 if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
32 $this->binlog
= $m[1]; // ideally something like host name
33 $this->pos
= [ (int)$m[2], (int)$m[3] ];
35 $gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
36 foreach ( $gtids as $gtid ) {
37 if ( !$this->parseGTID( $gtid ) ) {
38 throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
40 $this->gtids
[] = $gtid;
42 if ( !$this->gtids
) {
43 throw new InvalidArgumentException( "Got empty GTID set." );
47 $this->asOfTime
= $asOfTime;
50 public function asOfTime() {
51 return $this->asOfTime
;
54 public function hasReached( DBMasterPos
$pos ) {
55 if ( !( $pos instanceof self
) ) {
56 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__
);
59 // Prefer GTID comparisons, which work with multi-tier replication
60 $thisPosByDomain = $this->getGtidCoordinates();
61 $thatPosByDomain = $pos->getGtidCoordinates();
62 if ( $thisPosByDomain && $thatPosByDomain ) {
64 // Check that this has positions reaching those in $pos for all domains in common
65 foreach ( $thatPosByDomain as $domain => $thatPos ) {
66 if ( isset( $thisPosByDomain[$domain] ) ) {
67 $comparisons[] = ( $thatPos <= $thisPosByDomain[$domain] );
70 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
71 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
72 // be cleaned up. Assume that the domains in both this and $pos cover the relevant
74 return ( $comparisons && !in_array( false, $comparisons, true ) );
77 // Fallback to the binlog file comparisons
78 $thisBinPos = $this->getBinlogCoordinates();
79 $thatBinPos = $pos->getBinlogCoordinates();
80 if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
81 return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
84 // Comparing totally different binlogs does not make sense
88 public function channelsMatch( DBMasterPos
$pos ) {
89 if ( !( $pos instanceof self
) ) {
90 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__
);
93 // Prefer GTID comparisons, which work with multi-tier replication
94 $thisPosDomains = array_keys( $this->getGtidCoordinates() );
95 $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
96 if ( $thisPosDomains && $thatPosDomains ) {
97 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
98 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
99 // easily be cleaned up. Assume that the domains in both this and $pos cover the
100 // relevant active channels.
101 return array_intersect( $thatPosDomains, $thisPosDomains ) ?
true : false;
104 // Fallback to the binlog file comparisons
105 $thisBinPos = $this->getBinlogCoordinates();
106 $thatBinPos = $pos->getBinlogCoordinates();
108 return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
112 * @return string|null
114 public function getLogFile() {
115 return $this->gtids ?
null : "{$this->binlog}.{$this->pos[0]}";
121 public function getGTIDs() {
126 * @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
128 public function __toString() {
130 ?
implode( ',', $this->gtids
)
131 : $this->getLogFile() . "/{$this->pos[1]}";
135 * @param MySQLMasterPos $pos
136 * @param MySQLMasterPos $refPos
137 * @return string[] List of GTIDs from $pos that have domains in $refPos
139 public static function getCommonDomainGTIDs( MySQLMasterPos
$pos, MySQLMasterPos
$refPos ) {
142 $relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused)
143 foreach ( $pos->gtids
as $gtid ) {
144 list( $domain ) = self
::parseGTID( $gtid );
145 if ( isset( $relevantDomains[$domain] ) ) {
146 $gtidsCommon[] = $gtid;
154 * @see https://mariadb.com/kb/en/mariadb/gtid
155 * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
156 * @return array Map of (domain => integer position); possibly empty
158 protected function getGtidCoordinates() {
160 foreach ( $this->gtids
as $gtid ) {
161 list( $domain, $pos ) = self
::parseGTID( $gtid );
162 $gtidInfos[$domain] = $pos;
169 * @param string $gtid
170 * @return array|null [domain, integer position] or null
172 protected static function parseGTID( $gtid ) {
174 if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
175 // MariaDB style: <domain>-<server id>-<sequence number>
176 return [ (int)$m[1], (int)$m[2] ];
177 } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
178 // MySQL style: <UUID domain>:<sequence number>
179 return [ $m[1], (int)$m[2] ];
186 * @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
187 * @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
188 * @return array|bool (binlog, (integer file number, integer position)) or false
190 protected function getBinlogCoordinates() {
191 return ( $this->binlog
!== null && $this->pos
!== null )
192 ?
[ 'binlog' => $this->binlog
, 'pos' => $this->pos
]