X-Git-Url: http://git.cyclocoop.org/?a=blobdiff_plain;f=includes%2Flibs%2Frdbms%2Fdatabase%2FDatabaseMysqlBase.php;h=8fca440f67979370b8c8c0f9f68de0d784aad8af;hb=b7d1782e1055cd04338293d1c5ce0f09ffadbff4;hp=286d65881ec4cc7071bb786ddd7ae9e98f2e81f7;hpb=51eba1974c51c4874eb64d639fa5ae0eb56ef0d4;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/libs/rdbms/database/DatabaseMysqlBase.php b/includes/libs/rdbms/database/DatabaseMysqlBase.php index 286d65881e..8fca440f67 100644 --- a/includes/libs/rdbms/database/DatabaseMysqlBase.php +++ b/includes/libs/rdbms/database/DatabaseMysqlBase.php @@ -73,6 +73,9 @@ abstract class DatabaseMysqlBase extends Database { // Cache getServerId() for 24 hours const SERVER_ID_CACHE_TTL = 86400; + /** @var float Warn if lag estimates are made for transactions older than this many seconds */ + const LAG_STALE_WARN_THRESHOLD = 0.100; + /** * Additional $params include: * - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat). @@ -317,7 +320,7 @@ abstract class DatabaseMysqlBase extends Database { abstract protected function mysqlFetchObject( $res ); /** - * @param ResultWrapper|resource $res + * @param IResultWrapper|resource $res * @return array|bool * @throws DBUnexpectedError */ @@ -749,6 +752,7 @@ abstract class DatabaseMysqlBase extends Database { protected function getLagFromSlaveStatus() { $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); $row = $res ? $res->fetchObject() : false; + // If the server is not replicating, there will be no row if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) { return intval( $row->Seconds_Behind_Master ); } @@ -762,6 +766,22 @@ abstract class DatabaseMysqlBase extends Database { protected function getLagFromPtHeartbeat() { $options = $this->lagDetectionOptions; + $currentTrxInfo = $this->getRecordedTransactionLagStatus(); + if ( $currentTrxInfo ) { + // There is an active transaction and the initial lag was already queried + $staleness = microtime( true ) - $currentTrxInfo['since']; + if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) { + // Avoid returning higher and higher lag value due to snapshot age + // given that the isolation level will typically be REPEATABLE-READ + $this->queryLogger->warning( + "Using cached lag value for {db_server} due to active transaction", + $this->getLogContext( [ 'method' => __METHOD__, 'age' => $staleness ] ) + ); + } + + return $currentTrxInfo['lag']; + } + if ( isset( $options['conds'] ) ) { // Best method for multi-DC setups: use logical channel names $data = $this->getHeartbeatData( $options['conds'] ); @@ -856,7 +876,8 @@ abstract class DatabaseMysqlBase extends Database { // 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 $whereSQL ORDER BY ts DESC LIMIT 1" + "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1", + __METHOD__ ); $row = $res ? $res->fetchObject() : false; } finally { @@ -901,27 +922,29 @@ abstract class DatabaseMysqlBase extends Database { $rpos = $this->getReplicaPos(); $gtidsWait = $rpos ? MySQLMasterPos::getCommonDomainGTIDs( $pos, $rpos ) : []; if ( !$gtidsWait ) { + $this->queryLogger->error( + "No GTIDs with the same domain between master ($pos) and replica ($rpos)", + $this->getLogContext( [ + 'method' => __METHOD__, + ] ) + ); + return -1; // $pos is from the wrong cluster? } // Wait on the GTID set (MariaDB only) $gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) ); - if ( strpos( $gtidArg, ':' ) !== false ) { - // MySQL GTIDs, e.g "source_id:transaction_id" - $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" ); - } else { - // MariaDB GTIDs, e.g."domain:server:sequence" - $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" ); - } + $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" ); } else { // Wait on the binlog coordinates $encFile = $this->addQuotes( $pos->getLogFile() ); - $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] ); + $encPos = intval( $pos->pos[1] ); $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" ); } $row = $res ? $this->fetchRow( $res ) : false; if ( !$row ) { - throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" ); + throw new DBExpectedError( $this, + "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" ); } // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual @@ -953,23 +976,21 @@ abstract class DatabaseMysqlBase extends Database { * @return MySQLMasterPos|bool */ public function getReplicaPos() { - $now = microtime( true ); // as-of-time *before* fetching GTID variables - - if ( $this->useGTIDs() ) { - // Try to use GTIDs, fallbacking to binlog positions if not possible - $data = $this->getServerGTIDs( __METHOD__ ); - // Use gtid_current_pos for MariaDB and gtid_executed for MySQL - foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) { - if ( isset( $data[$name] ) && strlen( $data[$name] ) ) { - return new MySQLMasterPos( $data[$name], $now ); - } + $now = microtime( true ); + + if ( $this->useGTIDs ) { + $res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ ); + $gtidRow = $this->fetchObject( $res ); + if ( $gtidRow && strlen( $gtidRow->Value ) ) { + return new MySQLMasterPos( $gtidRow->Value, $now ); } } - $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ ); - if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) { + $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); + $row = $this->fetchObject( $res ); + if ( $row && strlen( $row->Relay_Master_Log_File ) ) { return new MySQLMasterPos( - "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}", + "{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}", $now ); } @@ -983,97 +1004,23 @@ abstract class DatabaseMysqlBase extends Database { * @return MySQLMasterPos|bool */ public function getMasterPos() { - $now = microtime( true ); // as-of-time *before* fetching GTID variables - - $pos = false; - if ( $this->useGTIDs() ) { - // Try to use GTIDs, fallbacking to binlog positions if not possible - $data = $this->getServerGTIDs( __METHOD__ ); - // Use gtid_current_pos for MariaDB and gtid_executed for MySQL - foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) { - if ( isset( $data[$name] ) && strlen( $data[$name] ) ) { - $pos = new MySQLMasterPos( $data[$name], $now ); - break; - } - } - // Filter domains that are inactive or not relevant to the session - if ( $pos ) { - $pos->setActiveOriginServerId( $this->getServerId() ); - $pos->setActiveOriginServerUUID( $this->getServerUUID() ); - if ( isset( $data['gtid_domain_id'] ) ) { - $pos->setActiveDomain( $data['gtid_domain_id'] ); - } - } - } + $now = microtime( true ); - if ( !$pos ) { - $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ ); - if ( $data && strlen( $data['File'] ) ) { - $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now ); + if ( $this->useGTIDs ) { + $res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ ); + $gtidRow = $this->fetchObject( $res ); + if ( $gtidRow && strlen( $gtidRow->Value ) ) { + return new MySQLMasterPos( $gtidRow->Value, $now ); } } - return $pos; - } - - /** - * @return int - * @throws DBQueryError If the variable doesn't exist for some reason - */ - protected function getServerId() { - return $this->srvCache->getWithSetCallback( - $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ), - self::SERVER_ID_CACHE_TTL, - function () { - $res = $this->query( "SELECT @@server_id AS id", __METHOD__ ); - return intval( $this->fetchObject( $res )->id ); - } - ); - } - - /** - * @return string|null - */ - protected function getServerUUID() { - return $this->srvCache->getWithSetCallback( - $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ), - self::SERVER_ID_CACHE_TTL, - function () { - $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" ); - $row = $this->fetchObject( $res ); - - return $row ? $row->Value : null; - } - ); - } - - /** - * @param string $fname - * @return string[] - */ - protected function getServerGTIDs( $fname = __METHOD__ ) { - $map = []; - // Get global-only variables like gtid_executed - $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname ); - foreach ( $res as $row ) { - $map[$row->Variable_name] = $row->Value; - } - // Get session-specific (e.g. gtid_domain_id since that is were writes will log) - $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname ); - foreach ( $res as $row ) { - $map[$row->Variable_name] = $row->Value; + $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ ); + $row = $this->fetchObject( $res ); + if ( $row && strlen( $row->File ) ) { + return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now ); } - return $map; - } - - /** - * @param string $role One of "MASTER"/"SLAVE" - * @param string $fname - * @return string[] Latest available server status row - */ - protected function getServerRoleStatus( $role, $fname = __METHOD__ ) { - return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: []; + return false; } public function serverIsReadOnly() { @@ -1518,12 +1465,6 @@ abstract class DatabaseMysqlBase extends Database { return 'CAST( ' . $field . ' AS SIGNED )'; } - /* - * @return bool Whether GTID support is used (mockable for testing) - */ - protected function useGTIDs() { - return $this->useGTIDs; - } } class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );