Merge "Add support for 'hu-formal'"
[lhc/web/wiklou.git] / includes / libs / rdbms / database / position / MySQLMasterPos.php
1 <?php
2
3 namespace Wikimedia\Rdbms;
4
5 use InvalidArgumentException;
6 use UnexpectedValueException;
7
8 /**
9 * DBMasterPos class for MySQL/MariaDB
10 *
11 * Note that master positions and sync logic here make some assumptions:
12 * - Binlog-based usage assumes single-source replication and non-hierarchical replication.
13 * - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
14 * that GTID sets are complete (e.g. include all domains on the server).
15 */
16 class MySQLMasterPos implements DBMasterPos {
17 /** @var string|null Binlog file base name */
18 public $binlog;
19 /** @var int[]|null Binglog file position tuple */
20 public $pos;
21 /** @var string[] GTID list */
22 public $gtids = [];
23 /** @var float UNIX timestamp */
24 public $asOfTime = 0.0;
25
26 /**
27 * @param string $position One of (comma separated GTID list, <binlog file>/<integer>)
28 * @param float $asOfTime UNIX timestamp
29 */
30 public function __construct( $position, $asOfTime ) {
31 $this->init( $position, $asOfTime );
32 }
33
34 /**
35 * @param string $position
36 * @param float $asOfTime
37 */
38 protected function init( $position, $asOfTime ) {
39 $m = [];
40 if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
41 $this->binlog = $m[1]; // ideally something like host name
42 $this->pos = [ (int)$m[2], (int)$m[3] ];
43 } else {
44 $gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
45 foreach ( $gtids as $gtid ) {
46 if ( !self::parseGTID( $gtid ) ) {
47 throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
48 }
49 $this->gtids[] = $gtid;
50 }
51 if ( !$this->gtids ) {
52 throw new InvalidArgumentException( "Got empty GTID set." );
53 }
54 }
55
56 $this->asOfTime = $asOfTime;
57 }
58
59 public function asOfTime() {
60 return $this->asOfTime;
61 }
62
63 public function hasReached( DBMasterPos $pos ) {
64 if ( !( $pos instanceof self ) ) {
65 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
66 }
67
68 // Prefer GTID comparisons, which work with multi-tier replication
69 $thisPosByDomain = $this->getGtidCoordinates();
70 $thatPosByDomain = $pos->getGtidCoordinates();
71 if ( $thisPosByDomain && $thatPosByDomain ) {
72 $comparisons = [];
73 // Check that this has positions reaching those in $pos for all domains in common
74 foreach ( $thatPosByDomain as $domain => $thatPos ) {
75 if ( isset( $thisPosByDomain[$domain] ) ) {
76 $comparisons[] = ( $thatPos <= $thisPosByDomain[$domain] );
77 }
78 }
79 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
80 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
81 // be cleaned up. Assume that the domains in both this and $pos cover the relevant
82 // active channels.
83 return ( $comparisons && !in_array( false, $comparisons, true ) );
84 }
85
86 // Fallback to the binlog file comparisons
87 $thisBinPos = $this->getBinlogCoordinates();
88 $thatBinPos = $pos->getBinlogCoordinates();
89 if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
90 return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
91 }
92
93 // Comparing totally different binlogs does not make sense
94 return false;
95 }
96
97 public function channelsMatch( DBMasterPos $pos ) {
98 if ( !( $pos instanceof self ) ) {
99 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
100 }
101
102 // Prefer GTID comparisons, which work with multi-tier replication
103 $thisPosDomains = array_keys( $this->getGtidCoordinates() );
104 $thatPosDomains = array_keys( $pos->getGtidCoordinates() );
105 if ( $thisPosDomains && $thatPosDomains ) {
106 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
107 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
108 // easily be cleaned up. Assume that the domains in both this and $pos cover the
109 // relevant active channels.
110 return array_intersect( $thatPosDomains, $thisPosDomains ) ? true : false;
111 }
112
113 // Fallback to the binlog file comparisons
114 $thisBinPos = $this->getBinlogCoordinates();
115 $thatBinPos = $pos->getBinlogCoordinates();
116
117 return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
118 }
119
120 /**
121 * @return string|null
122 */
123 public function getLogFile() {
124 return $this->gtids ? null : "{$this->binlog}.{$this->pos[0]}";
125 }
126
127 /**
128 * @return string[]
129 */
130 public function getGTIDs() {
131 return $this->gtids;
132 }
133
134 /**
135 * @return string GTID set or <binlog file>/<position> (e.g db1034-bin.000976/843431247)
136 */
137 public function __toString() {
138 return $this->gtids
139 ? implode( ',', $this->gtids )
140 : $this->getLogFile() . "/{$this->pos[1]}";
141 }
142
143 /**
144 * @param MySQLMasterPos $pos
145 * @param MySQLMasterPos $refPos
146 * @return string[] List of GTIDs from $pos that have domains in $refPos
147 */
148 public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
149 $gtidsCommon = [];
150
151 $relevantDomains = $refPos->getGtidCoordinates(); // (domain => unused)
152 foreach ( $pos->gtids as $gtid ) {
153 list( $domain ) = self::parseGTID( $gtid );
154 if ( isset( $relevantDomains[$domain] ) ) {
155 $gtidsCommon[] = $gtid;
156 }
157 }
158
159 return $gtidsCommon;
160 }
161
162 /**
163 * @see https://mariadb.com/kb/en/mariadb/gtid
164 * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
165 * @return array Map of (domain => integer position); possibly empty
166 */
167 protected function getGtidCoordinates() {
168 $gtidInfos = [];
169 foreach ( $this->gtids as $gtid ) {
170 list( $domain, $pos ) = self::parseGTID( $gtid );
171 $gtidInfos[$domain] = $pos;
172 }
173
174 return $gtidInfos;
175 }
176
177 /**
178 * @param string $gtid
179 * @return array|null [domain, integer position] or null
180 */
181 protected static function parseGTID( $gtid ) {
182 $m = [];
183 if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
184 // MariaDB style: <domain>-<server id>-<sequence number>
185 return [ (int)$m[1], (int)$m[2] ];
186 } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
187 // MySQL style: <UUID domain>:<sequence number>
188 return [ $m[1], (int)$m[2] ];
189 }
190
191 return null;
192 }
193
194 /**
195 * @see https://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
196 * @see https://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
197 * @return array|bool (binlog, (integer file number, integer position)) or false
198 */
199 protected function getBinlogCoordinates() {
200 return ( $this->binlog !== null && $this->pos !== null )
201 ? [ 'binlog' => $this->binlog, 'pos' => $this->pos ]
202 : false;
203 }
204
205 public function serialize() {
206 return serialize( [ 'position' => $this->__toString(), 'asOfTime' => $this->asOfTime ] );
207 }
208
209 public function unserialize( $serialized ) {
210 $data = unserialize( $serialized );
211 if ( !is_array( $data ) ) {
212 throw new UnexpectedValueException( __METHOD__ . ": cannot unserialize position" );
213 }
214
215 $this->init( $data['position'], $data['asOfTime'] );
216 }
217 }