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