MediaWiki REL1_35
MySQLMasterPos.php
Go to the documentation of this file.
1<?php
2
3namespace Wikimedia\Rdbms;
4
5use InvalidArgumentException;
6use UnexpectedValueException;
7
19class MySQLMasterPos implements DBMasterPos {
21 private $style;
23 private $binLog;
25 private $logPos;
27 private $gtids = [];
35 private $asOfTime = 0.0;
36
37 private const BINARY_LOG = 'binary-log';
38 private const GTID_MARIA = 'gtid-maria';
39 private const GTID_MYSQL = 'gtid-mysql';
40
42 public const CORD_INDEX = 0;
44 public const CORD_EVENT = 1;
45
50 public function __construct( $position, $asOfTime ) {
51 $this->init( $position, $asOfTime );
52 }
53
58 protected function init( $position, $asOfTime ) {
59 $m = [];
60 if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', $position, $m ) ) {
61 $this->binLog = $m[1]; // ideally something like host name
62 $this->logPos = [ self::CORD_INDEX => (int)$m[2], self::CORD_EVENT => (int)$m[3] ];
63 $this->style = self::BINARY_LOG;
64 } else {
65 $gtids = array_filter( array_map( 'trim', explode( ',', $position ) ) );
66 foreach ( $gtids as $gtid ) {
67 $components = self::parseGTID( $gtid );
68 if ( !$components ) {
69 throw new InvalidArgumentException( "Invalid GTID '$gtid'." );
70 }
71
72 list( $domain, $pos ) = $components;
73 if ( isset( $this->gtids[$domain] ) ) {
74 // For MySQL, handle the case where some past issue caused a gap in the
75 // executed GTID set, e.g. [last_purged+1,N-1] and [N+1,N+2+K]. Ignore the
76 // gap by using the GTID with the highest ending sequence number.
77 list( , $otherPos ) = self::parseGTID( $this->gtids[$domain] );
78 if ( $pos > $otherPos ) {
79 $this->gtids[$domain] = $gtid;
80 }
81 } else {
82 $this->gtids[$domain] = $gtid;
83 }
84
85 if ( is_int( $domain ) ) {
86 $this->style = self::GTID_MARIA; // gtid_domain_id
87 } else {
88 $this->style = self::GTID_MYSQL; // server_uuid
89 }
90 }
91 if ( !$this->gtids ) {
92 throw new InvalidArgumentException( "GTID set cannot be empty." );
93 }
94 }
95
96 $this->asOfTime = $asOfTime;
97 }
98
99 public function asOfTime() {
100 return $this->asOfTime;
101 }
102
103 public function hasReached( DBMasterPos $pos ) {
104 if ( !( $pos instanceof self ) ) {
105 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
106 }
107
108 // Prefer GTID comparisons, which work with multi-tier replication
109 $thisPosByDomain = $this->getActiveGtidCoordinates();
110 $thatPosByDomain = $pos->getActiveGtidCoordinates();
111 if ( $thisPosByDomain && $thatPosByDomain ) {
112 $comparisons = [];
113 // Check that this has positions reaching those in $pos for all domains in common
114 foreach ( $thatPosByDomain as $domain => $thatPos ) {
115 if ( isset( $thisPosByDomain[$domain] ) ) {
116 $comparisons[] = ( $thatPos <= $thisPosByDomain[$domain] );
117 }
118 }
119 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
120 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
121 // be cleaned up. Assume that the domains in both this and $pos cover the relevant
122 // active channels.
123 return ( $comparisons && !in_array( false, $comparisons, true ) );
124 }
125
126 // Fallback to the binlog file comparisons
127 $thisBinPos = $this->getBinlogCoordinates();
128 $thatBinPos = $pos->getBinlogCoordinates();
129 if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
130 return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
131 }
132
133 // Comparing totally different binlogs does not make sense
134 return false;
135 }
136
137 public function channelsMatch( DBMasterPos $pos ) {
138 if ( !( $pos instanceof self ) ) {
139 throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
140 }
141
142 // Prefer GTID comparisons, which work with multi-tier replication
143 $thisPosDomains = array_keys( $this->getActiveGtidCoordinates() );
144 $thatPosDomains = array_keys( $pos->getActiveGtidCoordinates() );
145 if ( $thisPosDomains && $thatPosDomains ) {
146 // Check that $this has a GTID for at least one domain also in $pos; due to MariaDB
147 // quirks, prior master switch-overs may result in inactive garbage GTIDs that cannot
148 // easily be cleaned up. Assume that the domains in both this and $pos cover the
149 // relevant active channels.
150 return array_intersect( $thatPosDomains, $thisPosDomains ) ? true : false;
151 }
152
153 // Fallback to the binlog file comparisons
154 $thisBinPos = $this->getBinlogCoordinates();
155 $thatBinPos = $pos->getBinlogCoordinates();
156
157 return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
158 }
159
164 public function getLogName() {
165 return $this->gtids ? null : $this->binLog;
166 }
167
172 public function getLogPosition() {
173 return $this->gtids ? null : $this->logPos;
174 }
175
180 public function getLogFile() {
181 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
182 return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}";
183 }
184
189 public function getGTIDs() {
190 return $this->gtids;
191 }
192
205 public function setActiveDomain( $id ) {
206 $this->activeDomain = (int)$id;
207
208 return $this;
209 }
210
222 public function setActiveOriginServerId( $id ) {
223 $this->activeServerId = (int)$id;
224
225 return $this;
226 }
227
239 public function setActiveOriginServerUUID( $id ) {
240 $this->activeServerUUID = $id;
241
242 return $this;
243 }
244
251 public static function getRelevantActiveGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
252 return array_values( array_intersect_key(
253 $pos->gtids,
255 $refPos->gtids
256 ) );
257 }
258
264 protected function getActiveGtidCoordinates() {
265 $gtidInfos = [];
266
267 foreach ( $this->gtids as $domain => $gtid ) {
268 list( $domain, $pos, $server ) = self::parseGTID( $gtid );
269
270 $ignore = false;
271 // Filter out GTIDs from non-active replication domains
272 if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
273 $ignore = $ignore || ( $domain !== $this->activeDomain );
274 }
275 // Likewise for GTIDs from non-active replication origin servers
276 if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
277 $ignore = $ignore || ( $server !== $this->activeServerId );
278 } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
279 $ignore = $ignore || ( $server !== $this->activeServerUUID );
280 }
281
282 if ( !$ignore ) {
283 $gtidInfos[$domain] = $pos;
284 }
285 }
286
287 return $gtidInfos;
288 }
289
294 protected static function parseGTID( $id ) {
295 $m = [];
296 if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
297 // MariaDB style: <domain>-<server id>-<sequence number>
298 return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
299 } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
300 // MySQL style: <server UUID>:<sequence number>-<sequence number>
301 // Normally, the first number should reflect the point (gtid_purged) where older
302 // binary logs where purged to save space. When doing comparisons, it may as well
303 // be 1 in that case. Assume that this is generally the situation.
304 return [ $m[1], (int)$m[2], $m[1] ];
305 }
306
307 return null;
308 }
309
315 protected function getBinlogCoordinates() {
316 return ( $this->binLog !== null && $this->logPos !== null )
317 ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
318 : false;
319 }
320
321 public function serialize() {
322 return serialize( [
323 'position' => $this->__toString(),
324 'activeDomain' => $this->activeDomain,
325 'activeServerId' => $this->activeServerId,
326 'activeServerUUID' => $this->activeServerUUID,
327 'asOfTime' => $this->asOfTime
328 ] );
329 }
330
331 public function unserialize( $serialized ) {
332 $data = unserialize( $serialized );
333 if ( !is_array( $data ) ) {
334 throw new UnexpectedValueException( __METHOD__ . ": cannot unserialize position" );
335 }
336
337 $this->init( $data['position'], $data['asOfTime'] );
338 if ( isset( $data['activeDomain'] ) ) {
339 $this->setActiveDomain( $data['activeDomain'] );
340 }
341 if ( isset( $data['activeServerId'] ) ) {
342 $this->setActiveOriginServerId( $data['activeServerId'] );
343 }
344 if ( isset( $data['activeServerUUID'] ) ) {
345 $this->setActiveOriginServerUUID( $data['activeServerUUID'] );
346 }
347 }
348
352 public function __toString() {
353 return $this->gtids
354 ? implode( ',', $this->gtids )
355 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
356 : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
357 }
358}
DBMasterPos class for MySQL/MariaDB.
string null $binLog
Base name of all Binary Log files.
init( $position, $asOfTime)
float $asOfTime
UNIX timestamp.
setActiveDomain( $id)
Set the GTID domain known to be used in new commits on a replication stream of interest.
string $style
One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA)
__construct( $position, $asOfTime)
static getRelevantActiveGTIDs(MySQLMasterPos $pos, MySQLMasterPos $refPos)
string[] $gtids
Map of (server_uuid/gtid_domain_id => GTID)
int null $activeDomain
Active GTID domain ID.
setActiveOriginServerId( $id)
Set the server ID known to be used in new commits on a replication stream of interest.
int null $activeServerId
ID of the server were DB writes originate.
string null $activeServerUUID
UUID of the server were DB writes originate.
int[] null $logPos
Binary Log position tuple (index number, event number)
setActiveOriginServerUUID( $id)
Set the server UUID known to be used in new commits on a replication stream of interest.
An object representing a master or replica DB position in a replicated setup.
return true
Definition router.php:92
foreach( $res as $row) $serialized