MediaWiki REL1_32
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 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
196 public function setActiveDomain( $id ) {
197 $this->activeDomain = (int)$id;
198 }
199
204 public function setActiveOriginServerId( $id ) {
205 $this->activeServerId = (int)$id;
206 }
207
212 public function setActiveOriginServerUUID( $id ) {
213 $this->activeServerUUID = $id;
214 }
215
222 public static function getCommonDomainGTIDs( MySQLMasterPos $pos, MySQLMasterPos $refPos ) {
223 return array_values(
224 array_intersect_key( $pos->gtids, $refPos->getActiveGtidCoordinates() )
225 );
226 }
227
233 protected function getActiveGtidCoordinates() {
234 $gtidInfos = [];
235
236 foreach ( $this->gtids as $domain => $gtid ) {
237 list( $domain, $pos, $server ) = self::parseGTID( $gtid );
238
239 $ignore = false;
240 // Filter out GTIDs from non-active replication domains
241 if ( $this->style === self::GTID_MARIA && $this->activeDomain !== null ) {
242 $ignore |= ( $domain !== $this->activeDomain );
243 }
244 // Likewise for GTIDs from non-active replication origin servers
245 if ( $this->style === self::GTID_MARIA && $this->activeServerId !== null ) {
246 $ignore |= ( $server !== $this->activeServerId );
247 } elseif ( $this->style === self::GTID_MYSQL && $this->activeServerUUID !== null ) {
248 $ignore |= ( $server !== $this->activeServerUUID );
249 }
250
251 if ( !$ignore ) {
252 $gtidInfos[$domain] = $pos;
253 }
254 }
255
256 return $gtidInfos;
257 }
258
263 protected static function parseGTID( $id ) {
264 $m = [];
265 if ( preg_match( '!^(\d+)-(\d+)-(\d+)$!', $id, $m ) ) {
266 // MariaDB style: <domain>-<server id>-<sequence number>
267 return [ (int)$m[1], (int)$m[3], (int)$m[2] ];
268 } elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(?:\d+-|)(\d+)$!', $id, $m ) ) {
269 // MySQL style: <server UUID>:<sequence number>-<sequence number>
270 // Normally, the first number should reflect the point (gtid_purged) where older
271 // binary logs where purged to save space. When doing comparisons, it may as well
272 // be 1 in that case. Assume that this is generally the situation.
273 return [ $m[1], (int)$m[2], $m[1] ];
274 }
275
276 return null;
277 }
278
284 protected function getBinlogCoordinates() {
285 return ( $this->binLog !== null && $this->logPos !== null )
286 ? [ 'binlog' => $this->binLog, 'pos' => $this->logPos ]
287 : false;
288 }
289
290 public function serialize() {
291 return serialize( [
292 'position' => $this->__toString(),
293 'activeDomain' => $this->activeDomain,
294 'activeServerId' => $this->activeServerId,
295 'activeServerUUID' => $this->activeServerUUID,
296 'asOfTime' => $this->asOfTime
297 ] );
298 }
299
300 public function unserialize( $serialized ) {
301 $data = unserialize( $serialized );
302 if ( !is_array( $data ) ) {
303 throw new UnexpectedValueException( __METHOD__ . ": cannot unserialize position" );
304 }
305
306 $this->init( $data['position'], $data['asOfTime'] );
307 if ( isset( $data['activeDomain'] ) ) {
308 $this->setActiveDomain( $data['activeDomain'] );
309 }
310 if ( isset( $data['activeServerId'] ) ) {
311 $this->setActiveOriginServerId( $data['activeServerId'] );
312 }
313 if ( isset( $data['activeServerUUID'] ) ) {
314 $this->setActiveOriginServerUUID( $data['activeServerUUID'] );
315 }
316 }
317
321 public function __toString() {
322 return $this->gtids
323 ? implode( ',', $this->gtids )
324 : $this->getLogFile() . "/{$this->logPos[self::CORD_EVENT]}";
325 }
326}
DBMasterPos class for MySQL/MariaDB.
string null $binLog
Base name of all Binary Log files.
init( $position, $asOfTime)
float $asOfTime
UNIX timestamp.
static getCommonDomainGTIDs(MySQLMasterPos $pos, MySQLMasterPos $refPos)
__construct( $position, $asOfTime)
int $style
One of (BINARY_LOG, GTID_MYSQL, GTID_MARIA)
string[] $gtids
Map of (server_uuid/gtid_domain_id => GTID)
int null $activeDomain
Active GTID domain ID.
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)
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2055
An object representing a master or replica DB position in a replicated setup.
foreach( $res as $row) $serialized
Bar style