MediaWiki  1.34.0
MySQLMasterPos.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Wikimedia\Rdbms;
4 
5 use InvalidArgumentException;
6 use UnexpectedValueException;
7 
19 class MySQLMasterPos implements DBMasterPos {
21  private $style;
23  private $binLog;
25  private $logPos;
27  private $gtids = [];
29  private $activeDomain;
31  private $activeServerId;
35  private $asOfTime = 0.0;
36 
37  const BINARY_LOG = 'binary-log';
38  const GTID_MARIA = 'gtid-maria';
39  const GTID_MYSQL = 'gtid-mysql';
40 
42  const CORD_INDEX = 0;
44  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  return $this->gtids ? null : "{$this->binLog}.{$this->logPos[self::CORD_INDEX]}";
182  }
183 
188  public function getGTIDs() {
189  return $this->gtids;
190  }
191 
204  public function setActiveDomain( $id ) {
205  $this->activeDomain = (int)$id;
206 
207  return $this;
208  }
209 
221  public function setActiveOriginServerId( $id ) {
222  $this->activeServerId = (int)$id;
223 
224  return $this;
225  }
226 
238  public function setActiveOriginServerUUID( $id ) {
239  $this->activeServerUUID = $id;
240 
241  return $this;
242  }
243 
250  public static function getRelevantActiveGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
251  return array_values( array_intersect_key(
252  $pos->gtids,
253  $pos->getActiveGtidCoordinates(),
254  $refPos->gtids
255  ) );
256  }
257 
263  protected function getActiveGtidCoordinates() {
264  $gtidInfos = [];
265 
266  foreach ( $this->gtids as $domain => $gtid ) {
267  list( $domain, $pos, $server ) = self::parseGTID( $gtid );
268 
269  $ignore = false;
270  // Filter out GTIDs from non-active replication domains
271  if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
272  $ignore |= ( $domain !== $this->activeDomain );
273  }
274  // Likewise for GTIDs from non-active replication origin servers
275  if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
276  $ignore |= ( $server !== $this->activeServerId );
277  } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
278  $ignore |= ( $server !== $this->activeServerUUID );
279  }
280 
281  if ( !$ignore ) {
282  $gtidInfos[$domain] = $pos;
283  }
284  }
285 
286  return $gtidInfos;
287  }
288 
293  protected static function parseGTID( $id ) {
294  $m = [];
295  if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
296  // MariaDB style: <domain>-<server id>-<sequence number>
297  return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
298  } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
299  // MySQL style: <server UUID>:<sequence number>-<sequence number>
300  // Normally, the first number should reflect the point (gtid_purged) where older
301  // binary logs where purged to save space. When doing comparisons, it may as well
302  // be 1 in that case. Assume that this is generally the situation.
303  return [ $m[1], (int)$m[2], $m[1] ];
304  }
305 
306  return null;
307  }
308 
314  protected function getBinlogCoordinates() {
315  return ( $this->binLog !== null && $this->logPos !== null )
316  ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
317  : false;
318  }
319 
320  public function serialize() {
321  return serialize( [
322  'position' => $this->__toString(),
323  'activeDomain' => $this->activeDomain,
324  'activeServerId' => $this->activeServerId,
325  'activeServerUUID' => $this->activeServerUUID,
326  'asOfTime' => $this->asOfTime
327  ] );
328  }
329 
330  public function unserialize( $serialized ) {
331  $data = unserialize( $serialized );
332  if ( !is_array( $data ) ) {
333  throw new UnexpectedValueException( __METHOD__ . ": cannot unserialize position" );
334  }
335 
336  $this->init( $data['position'], $data['asOfTime'] );
337  if ( isset( $data['activeDomain'] ) ) {
338  $this->setActiveDomain( $data['activeDomain'] );
339  }
340  if ( isset( $data['activeServerId'] ) ) {
341  $this->setActiveOriginServerId( $data['activeServerId'] );
342  }
343  if ( isset( $data['activeServerUUID'] ) ) {
344  $this->setActiveOriginServerUUID( $data['activeServerUUID'] );
345  }
346  }
347 
351  public function __toString() {
352  return $this->gtids
353  ? implode( ',', $this->gtids )
354  : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
355  }
356 }
Wikimedia\Rdbms\MySQLMasterPos\$activeDomain
int null $activeDomain
Active GTID domain ID.
Definition: MySQLMasterPos.php:29
Wikimedia\Rdbms\MySQLMasterPos\init
init( $position, $asOfTime)
Definition: MySQLMasterPos.php:58
Wikimedia\Rdbms\MySQLMasterPos\getBinlogCoordinates
getBinlogCoordinates()
Definition: MySQLMasterPos.php:314
true
return true
Definition: router.php:92
Wikimedia\Rdbms\MySQLMasterPos\asOfTime
asOfTime()
Definition: MySQLMasterPos.php:99
Wikimedia\Rdbms\MySQLMasterPos\setActiveDomain
setActiveDomain( $id)
Set the GTID domain known to be used in new commits on a replication stream of interest.
Definition: MySQLMasterPos.php:204
$serialized
foreach( $res as $row) $serialized
Definition: testCompression.php:81
Wikimedia\Rdbms\MySQLMasterPos\$gtids
string[] $gtids
Map of (server_uuid/gtid_domain_id => GTID)
Definition: MySQLMasterPos.php:27
Wikimedia\Rdbms
Definition: ChronologyProtector.php:24
Wikimedia\Rdbms\DBMasterPos
An object representing a master or replica DB position in a replicated setup.
Definition: DBMasterPos.php:12
Wikimedia\Rdbms\MySQLMasterPos\GTID_MARIA
const GTID_MARIA
Definition: MySQLMasterPos.php:38
Wikimedia\Rdbms\MySQLMasterPos\serialize
serialize()
Definition: MySQLMasterPos.php:320
Wikimedia\Rdbms\MySQLMasterPos\getLogFile
getLogFile()
Definition: MySQLMasterPos.php:180
Wikimedia\Rdbms\MySQLMasterPos\getLogPosition
getLogPosition()
Definition: MySQLMasterPos.php:172
Wikimedia\Rdbms\MySQLMasterPos\setActiveOriginServerUUID
setActiveOriginServerUUID( $id)
Set the server UUID known to be used in new commits on a replication stream of interest.
Definition: MySQLMasterPos.php:238
Wikimedia\Rdbms\MySQLMasterPos\__construct
__construct( $position, $asOfTime)
Definition: MySQLMasterPos.php:50
Wikimedia\Rdbms\MySQLMasterPos\setActiveOriginServerId
setActiveOriginServerId( $id)
Set the server ID known to be used in new commits on a replication stream of interest.
Definition: MySQLMasterPos.php:221
Wikimedia\Rdbms\MySQLMasterPos\channelsMatch
channelsMatch(DBMasterPos $pos)
Definition: MySQLMasterPos.php:137
Wikimedia\Rdbms\MySQLMasterPos\hasReached
hasReached(DBMasterPos $pos)
Definition: MySQLMasterPos.php:103
Wikimedia\Rdbms\MySQLMasterPos\$activeServerId
int null $activeServerId
ID of the server were DB writes originate.
Definition: MySQLMasterPos.php:31
Wikimedia\Rdbms\MySQLMasterPos
DBMasterPos class for MySQL/MariaDB.
Definition: MySQLMasterPos.php:19
Wikimedia\Rdbms\MySQLMasterPos\$logPos
int[] null $logPos
Binary Log position tuple (index number, event number)
Definition: MySQLMasterPos.php:25
Wikimedia\Rdbms\MySQLMasterPos\getActiveGtidCoordinates
getActiveGtidCoordinates()
Definition: MySQLMasterPos.php:263
Wikimedia\Rdbms\MySQLMasterPos\$asOfTime
float $asOfTime
UNIX timestamp.
Definition: MySQLMasterPos.php:35
Wikimedia\Rdbms\MySQLMasterPos\parseGTID
static parseGTID( $id)
Definition: MySQLMasterPos.php:293
Wikimedia\Rdbms\MySQLMasterPos\getLogName
getLogName()
Definition: MySQLMasterPos.php:164
Wikimedia\Rdbms\MySQLMasterPos\GTID_MYSQL
const GTID_MYSQL
Definition: MySQLMasterPos.php:39
Wikimedia\Rdbms\MySQLMasterPos\__toString
__toString()
Definition: MySQLMasterPos.php:351
Wikimedia\Rdbms\MySQLMasterPos\$style
string $style
One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA)
Definition: MySQLMasterPos.php:21
Wikimedia\Rdbms\MySQLMasterPos\BINARY_LOG
const BINARY_LOG
Definition: MySQLMasterPos.php:37
Wikimedia\Rdbms\MySQLMasterPos\$binLog
string null $binLog
Base name of all Binary Log files.
Definition: MySQLMasterPos.php:23
Wikimedia\Rdbms\MySQLMasterPos\$activeServerUUID
string null $activeServerUUID
UUID of the server were DB writes originate.
Definition: MySQLMasterPos.php:33
Wikimedia\Rdbms\MySQLMasterPos\getRelevantActiveGTIDs
static getRelevantActiveGTIDs(MySQLMasterPos $pos, MySQLMasterPos $refPos)
Definition: MySQLMasterPos.php:250
Wikimedia\Rdbms\MySQLMasterPos\unserialize
unserialize( $serialized)
Definition: MySQLMasterPos.php:330
Wikimedia\Rdbms\MySQLMasterPos\getGTIDs
getGTIDs()
Definition: MySQLMasterPos.php:188