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;
* 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 ) {
$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
? $params['lagDetectionMethod']
: 'Seconds_Behind_Master';
+ $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
+ ? $params['lagDetectionOptions']
+ : [];
}
/**
* @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' ) );
}
/**
- * @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;
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
# 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;
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)
*/