2ee90684466c79ed2dbc12ddab360cc5fb6d27f1
[lhc/web/wiklou.git] / includes / libs / rdbms / database / position / MySQLMasterPos.php
1 <?php
2
3 namespace Wikimedia\Rdbms;
4
5 use InvalidArgumentException;
6
7 /**
8 * DBMasterPos class for MySQL/MariaDB
9 *
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).
14 */
15 class MySQLMasterPos implements DBMasterPos {
16 /** @var string|null Binlog file base name */
17 public $binlog;
18 /** @var int[]|null Binglog file position tuple */
19 public $pos;
20 /** @var string[] GTID list */
21 public $gtids = [];
22 /** @var float UNIX timestamp */
23 public $asOfTime = 0.0;
24
25 /**
26 * @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
27 * @param float $asOfTime UNIX timestamp
28 */
29 public function __construct( $position, $asOfTime ) {
30 $m = [];
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] ];
34 } else {
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'." );
39 }
40 $this->gtids[] = $gtid;
41 }
42 if ( !$this->gtids ) {
43 throw new InvalidArgumentException( "Got empty GTID set." );
44 }
45 }
46
47 $this->asOfTime = $asOfTime;
48 }
49
50 public function asOfTime() {
51 return $this->asOfTime;
52 }
53
54 public function hasReached( DBMasterPos $pos ) {
55 if ( !( $pos instanceof self ) ) {
56 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
57 }
58
59 // Prefer GTID comparisons, which work with multi-tier replication
60 $thisPosByDomain = $this->getGtidCoordinates();
61 $thatPosByDomain = $pos->getGtidCoordinates();
62 if ( $thisPosByDomain && $thatPosByDomain ) {
63 $comparisons = [];
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] );
68 }
69 }
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
73 // active channels.
74 return ( $comparisons && !in_array( false, $comparisons, true ) );
75 }
76
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'] );
82 }
83
84 // Comparing totally different binlogs does not make sense
85 return false;
86 }
87
88 public function channelsMatch( DBMasterPos $pos ) {
89 if ( !( $pos instanceof self ) ) {
90 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
91 }
92
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;
102 }
103
104 // Fallback to the binlog file comparisons
105 $thisBinPos = $this->getBinlogCoordinates();
106 $thatBinPos = $pos->getBinlogCoordinates();
107
108 return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
109 }
110
111 /**
112 * @return string|null
113 */
114 public function getLogFile() {
115 return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}";
116 }
117
118 /**
119 * @return string[]
120 */
121 public function getGTIDs() {
122 return $this->gtids;
123 }
124
125 /**
126 * @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
127 */
128 public function __toString() {
129 return $this->gtids
130 ? implode( ',', $this->gtids )
131 : $this->getLogFile() . "/{$this->pos[1]}";
132 }
133
134 /**
135 * @param MySQLMasterPos $pos
136 * @param MySQLMasterPos $refPos
137 * @return string[] List of GTIDs from $pos that have domains in $refPos
138 */
139 public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
140 $gtidsCommon = [];
141
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;
147 }
148 }
149
150 return $gtidsCommon;
151 }
152
153 /**
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
157 */
158 protected function getGtidCoordinates() {
159 $gtidInfos = [];
160 foreach ( $this->gtids as $gtid ) {
161 list( $domain, $pos ) = self::parseGTID( $gtid );
162 $gtidInfos[$domain] = $pos;
163 }
164
165 return $gtidInfos;
166 }
167
168 /**
169 * @param string $gtid
170 * @return array|null [domain, integer position] or null
171 */
172 protected static function parseGTID( $gtid ) {
173 $m = [];
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] ];
180 }
181
182 return null;
183 }
184
185 /**
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
189 */
190 protected function getBinlogCoordinates() {
191 return ( $this->binlog !== null && $this->pos !== null )
192 ? [ 'binlog' => $this->binlog, 'pos' => $this->pos ]
193 : false;
194 }
195 }