X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fdb%2FDatabaseMysqlBase.php;h=13be9116869ba1262ca32c5e8087a37c8f71fc6f;hb=e08cd0de8432f1746a06a05b458f3d47280ae4aa;hp=7058061f8eddaa2f9b5ac78cbf712cacbe424e11;hpb=6e9b4f0e9ce4ccd6089c18b205065ef7fa077484;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index 7058061f8e..13be911686 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -34,6 +34,8 @@ abstract class DatabaseMysqlBase extends Database { protected $lastKnownSlavePos; /** @var string Method to detect slave lag */ protected $lagDetectionMethod; + /** @var array Method to detect slave lag */ + protected $lagDetectionOptions = []; /** @var string|null */ private $serverVersion = null; @@ -44,6 +46,10 @@ abstract class DatabaseMysqlBase extends Database { * pt-heartbeat assumes the table is at heartbeat.heartbeat * and uses UTC timestamps in the heartbeat.ts column. * (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html) + * - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change + * the default behavior. Normally, the heartbeat row with the server + * ID of this server's master will be used. Set the "conds" field to + * override the query conditions, e.g. ['shard' => 's1']. * @param array $params */ function __construct( array $params ) { @@ -52,6 +58,9 @@ abstract class DatabaseMysqlBase extends Database { $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] ) ? $params['lagDetectionMethod'] : 'Seconds_Behind_Master'; + $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] ) + ? $params['lagDetectionOptions'] + : []; } /** @@ -652,19 +661,30 @@ abstract class DatabaseMysqlBase extends Database { * @return bool|float */ protected function getLagFromPtHeartbeat() { - $masterInfo = $this->getMasterServerInfo(); - if ( !$masterInfo ) { - wfLogDBError( - "Unable to query master of {db_server} for server ID", - $this->getLogContext( [ - 'method' => __METHOD__ - ] ) - ); + $options = $this->lagDetectionOptions; + + if ( isset( $options['conds'] ) ) { + // Best method for multi-DC setups: use logical channel names + $data = $this->getHeartbeatData( $options['conds'] ); + } else { + // Standard method: use master server ID (works with stock pt-heartbeat) + $masterInfo = $this->getMasterServerInfo(); + if ( !$masterInfo ) { + wfLogDBError( + "Unable to query master of {db_server} for server ID", + $this->getLogContext( [ + 'method' => __METHOD__ + ] ) + ); - return false; // could not get master server ID + return false; // could not get master server ID + } + + $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ]; + $data = $this->getHeartbeatData( $conds ); } - list( $time, $nowUnix ) = $this->getHeartbeatData( $masterInfo['serverId'] ); + list( $time, $nowUnix ) = $data; if ( $time !== null ) { // @time is in ISO format like "2015-09-25T16:48:10.000510" $dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) ); @@ -722,17 +742,17 @@ abstract class DatabaseMysqlBase extends Database { } /** - * @param string $masterId Server ID - * @return array (heartbeat `ts` column value or null, UNIX timestamp) + * @param array $conds WHERE clause conditions to find a row + * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html */ - protected function getHeartbeatData( $masterId ) { - // Get the status row for this master; use the oldest for sanity in case the master - // has entries listed under different server IDs (which should really not happen). - // Note: this would use "MAX(TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6)))" but the + protected function getHeartbeatData( array $conds ) { + $whereSQL = $this->makeList( $conds, LIST_AND ); + // Use ORDER BY for channel based queries since that field might not be UNIQUE. + // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the // percision field is not supported in MySQL <= 5.5. $res = $this->query( - "SELECT ts FROM heartbeat.heartbeat WHERE server_id=" . intval( $masterId ) + "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1" ); $row = $res ? $res->fetchObject() : false; @@ -757,19 +777,13 @@ abstract class DatabaseMysqlBase extends Database { return $approxLag; } - /** - * Wait for the slave to catch up to a given master position. - * @todo Return values for this and base class are rubbish - * - * @param DBMasterPos|MySQLMasterPos $pos - * @param int $timeout The maximum number of seconds to wait for synchronisation - * @return int Zero if the slave was past that position already, - * greater than zero if we waited for some period of time, less than - * zero if we timed out. - */ function masterPosWait( DBMasterPos $pos, $timeout ) { + if ( !( $pos instanceof MySQLMasterPos ) ) { + throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" ); + } + if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) { - return '0'; // http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html + return 0; } # Commit any open transactions @@ -778,18 +792,28 @@ abstract class DatabaseMysqlBase extends Database { # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set $encFile = $this->addQuotes( $pos->file ); $encPos = intval( $pos->pos ); - $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; - $res = $this->doQuery( $sql ); - - $status = false; - if ( $res ) { - $row = $this->fetchRow( $res ); - if ( $row ) { - $status = $row[0]; // can be NULL, -1, or 0+ per the MySQL manual - if ( ctype_digit( $status ) ) { // success - $this->lastKnownSlavePos = $pos; - } + $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" ); + + $row = $res ? $this->fetchRow( $res ) : false; + if ( !$row ) { + throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" ); + } + + // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual + $status = ( $row[0] !== null ) ? intval( $row[0] ) : null; + if ( $status === null ) { + // T126436: jobs programmed to wait on master positions might be referencing binlogs + // with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try + // to detect this and treat the slave as having reached the position; a proper master + // switchover already requires that the new master be caught up before the switch. + $slavePos = $this->getSlavePos(); + if ( $slavePos && !$slavePos->channelsMatch( $pos ) ) { + $this->lastKnownSlavePos = $slavePos; + $status = 0; } + } elseif ( $status >= 0 ) { + // Remember that this position was reached to save queries next time + $this->lastKnownSlavePos = $pos; } return $status; @@ -1446,11 +1470,34 @@ class MySQLMasterPos implements DBMasterPos { return ( $thisPos && $thatPos && $thisPos >= $thatPos ); } + function channelsMatch( DBMasterPos $pos ) { + if ( !( $pos instanceof self ) ) { + throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ ); + } + + $thisBinlog = $this->getBinlogName(); + $thatBinlog = $pos->getBinlogName(); + + return ( $thisBinlog !== false && $thisBinlog === $thatBinlog ); + } + function __toString() { // e.g db1034-bin.000976/843431247 return "{$this->file}/{$this->pos}"; } + /** + * @return string|bool + */ + protected function getBinlogName() { + $m = []; + if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) { + return $m[1]; + } + + return false; + } + /** * @return array|bool (int, int) */