Revert "rdbms: make automatic connection recovery apply to more cases"
This reverts commit 4cac31de4e.
Reason for revert: Blocking the train, reverting the chain.
Change-Id: I7f275b3a25379c6f3256e90947c8eed4b232c0f4
This commit is contained in:
parent
226da4c3a0
commit
31c1ca8658
17 changed files with 218 additions and 901 deletions
|
|
@ -1815,7 +1815,6 @@ $wgAutoloadLocalClasses = [
|
|||
'Wikimedia\\Rdbms\\DBReadOnlyError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyError.php',
|
||||
'Wikimedia\\Rdbms\\DBReadOnlyRoleError' => __DIR__ . '/includes/libs/rdbms/exception/DBReadOnlyRoleError.php',
|
||||
'Wikimedia\\Rdbms\\DBReplicationWaitError' => __DIR__ . '/includes/libs/rdbms/exception/DBReplicationWaitError.php',
|
||||
'Wikimedia\\Rdbms\\DBSessionStateError' => __DIR__ . '/includes/libs/rdbms/exception/DBSessionStateError.php',
|
||||
'Wikimedia\\Rdbms\\DBTransactionError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionError.php',
|
||||
'Wikimedia\\Rdbms\\DBTransactionSizeError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionSizeError.php',
|
||||
'Wikimedia\\Rdbms\\DBTransactionStateError' => __DIR__ . '/includes/libs/rdbms/exception/DBTransactionStateError.php',
|
||||
|
|
|
|||
|
|
@ -133,9 +133,7 @@ class MWExceptionHandler {
|
|||
// to rollback some databases due to connection issues or exceptions.
|
||||
// However, any sensible DB driver will rollback implicitly anyway.
|
||||
try {
|
||||
$lbFactory = $services->getDBLoadBalancerFactory();
|
||||
$lbFactory->rollbackPrimaryChanges( __METHOD__ );
|
||||
$lbFactory->flushPrimarySessions( __METHOD__ );
|
||||
$services->getDBLoadBalancerFactory()->rollbackPrimaryChanges( __METHOD__ );
|
||||
} catch ( DBError $e2 ) {
|
||||
// If the DB is unreachable, rollback() will throw an error
|
||||
// and the error report() method might need messages from the DB,
|
||||
|
|
|
|||
|
|
@ -617,10 +617,6 @@ class DBConnRef implements IDatabase {
|
|||
return $this->__call( __FUNCTION__, func_get_args() );
|
||||
}
|
||||
|
||||
public function flushSession( $fname = __METHOD__, $owner = null ) {
|
||||
return $this->__call( __FUNCTION__, func_get_args() );
|
||||
}
|
||||
|
||||
public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
|
||||
return $this->__call( __FUNCTION__, func_get_args() );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,10 +119,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
/** @var int[] Prior flags member variable values */
|
||||
private $priorFlags = [];
|
||||
|
||||
/** @var array<string,array> Map of (name => (UNIX time,trx ID)) for current lock() mutexes */
|
||||
/** @var array<string,float> Map of (name => UNIX timestamp) for locks obtained via lock() */
|
||||
protected $sessionNamedLocks = [];
|
||||
/** @var array<string,array> Map of (name => (type,pristine,trx ID)) for current temp tables */
|
||||
/** @var array Map of (table name => 1) for current TEMPORARY tables */
|
||||
protected $sessionTempTables = [];
|
||||
/** @var array Map of (table name => 1) for current TEMPORARY tables */
|
||||
protected $sessionDirtyTempTables = [];
|
||||
|
||||
/** @var array|null Replication lag estimate at the time of BEGIN for the last transaction */
|
||||
private $trxReplicaLagStatus = null;
|
||||
|
|
@ -163,20 +165,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
/** @var int New Database instance will already be connected when returned */
|
||||
public const NEW_CONNECTED = 1;
|
||||
|
||||
/** No errors occurred during the query */
|
||||
protected const ERR_NONE = 0;
|
||||
/** Retry query due to a connection loss detected while sending the query (session intact) */
|
||||
protected const ERR_RETRY_QUERY = 1;
|
||||
/** Abort query (no retries) due to a statement rollback (session/transaction intact) */
|
||||
protected const ERR_ABORT_QUERY = 2;
|
||||
/** Abort any current transaction, by rolling it back, due to an error during the query */
|
||||
protected const ERR_ABORT_TRX = 4;
|
||||
/** Abort and reset session due to server-side session-level state loss (locks, temp tables) */
|
||||
protected const ERR_ABORT_SESSION = 8;
|
||||
|
||||
/** Assume that queries taking this long to yield connection loss errors are at fault */
|
||||
protected const DROPPED_CONN_BLAME_THRESHOLD_SEC = 3.0;
|
||||
|
||||
/** @var string Idiom used when a cancelable atomic section started the transaction */
|
||||
private const NOT_APPLICABLE = 'n/a';
|
||||
|
||||
|
|
@ -558,32 +546,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the status of the current transaction
|
||||
*
|
||||
* @return int One of the TransactionManager::STATUS_TRX_* class constants
|
||||
* @return int One of the STATUS_TRX_* class constants
|
||||
* @since 1.31
|
||||
* @internal This method should not be used outside of Database/LoadBalancer
|
||||
*/
|
||||
public function trxStatus() {
|
||||
return $this->transactionManager->trxStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get important session state that cannot be recovered upon connection loss
|
||||
*
|
||||
* @return array<string,mixed> Map of (property => value) for the current session state
|
||||
*/
|
||||
private function sessionStateInfo() {
|
||||
return [
|
||||
'trxId' => $this->transactionManager->getTrxId(),
|
||||
'trxExplicit' => $this->transactionManager->explicitTrxActive(),
|
||||
'trxWriteCallers' => $this->transactionManager->pendingWriteCallers(),
|
||||
'trxPreCommitCbCallers' => $this->transactionManager->pendingPreCommitCallbackCallers(),
|
||||
'sessNamedLocks' => $this->sessionNamedLocks,
|
||||
'sessTempTables' => $this->sessionTempTables
|
||||
];
|
||||
}
|
||||
|
||||
public function tablePrefix( $prefix = null ) {
|
||||
$old = $this->currentDomain->getTablePrefix();
|
||||
|
||||
|
|
@ -864,7 +833,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
[
|
||||
'db_server' => $this->getServerName(),
|
||||
'db_name' => $this->getDBname(),
|
||||
'db_user' => $this->connectionParams[self::CONN_USER] ?? null,
|
||||
'db_user' => $this->connectionParams[self::CONN_USER],
|
||||
],
|
||||
$extras
|
||||
);
|
||||
|
|
@ -1001,7 +970,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
*
|
||||
* - Reject write queries to replica DBs, in query().
|
||||
*
|
||||
* @param string $sql SQL query
|
||||
* @param string $sql
|
||||
* @param int $flags Query flags to query()
|
||||
* @return bool
|
||||
*/
|
||||
|
|
@ -1037,16 +1006,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $sql SQL query
|
||||
* @param string $sql
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getQueryVerb( $sql ) {
|
||||
// Distinguish ROLLBACK from ROLLBACK TO SAVEPOINT
|
||||
return preg_match(
|
||||
'/^\s*(rollback\s+to\s+savepoint|[a-z]+)/i',
|
||||
$sql,
|
||||
$m
|
||||
) ? strtoupper( $m[1] ) : null;
|
||||
return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1066,18 +1030,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
protected function isTransactableQuery( $sql ) {
|
||||
return !in_array(
|
||||
$this->getQueryVerb( $sql ),
|
||||
[
|
||||
'BEGIN',
|
||||
'ROLLBACK',
|
||||
'ROLLBACK TO SAVEPOINT',
|
||||
'COMMIT',
|
||||
'SET',
|
||||
'SHOW',
|
||||
'CREATE',
|
||||
'ALTER',
|
||||
'USE',
|
||||
'SHOW'
|
||||
],
|
||||
[ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
|
@ -1130,10 +1083,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
if ( $queryVerb === 'CREATE' ) {
|
||||
// Record the type of temporary table being created
|
||||
$tableType = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
|
||||
} elseif ( isset( $this->sessionTempTables[$table] ) ) {
|
||||
$tableType = $this->sessionTempTables[$table]['type'];
|
||||
} else {
|
||||
$tableType = null;
|
||||
$tableType = $this->sessionTempTables[$table] ?? null;
|
||||
}
|
||||
|
||||
if ( $tableType !== null ) {
|
||||
|
|
@ -1156,24 +1107,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
|
||||
switch ( $verb ) {
|
||||
case 'CREATE':
|
||||
$this->sessionTempTables[$table] = [
|
||||
'type' => $tmpTableType,
|
||||
'pristine' => true,
|
||||
'trxId' => $this->transactionManager->getTrxId()
|
||||
];
|
||||
$this->sessionTempTables[$table] = $tmpTableType;
|
||||
break;
|
||||
case 'DROP':
|
||||
unset( $this->sessionTempTables[$table] );
|
||||
unset( $this->sessionDirtyTempTables[$table] );
|
||||
break;
|
||||
case 'TRUNCATE':
|
||||
if ( isset( $this->sessionTempTables[$table] ) ) {
|
||||
$this->sessionTempTables[$table]['pristine'] = true;
|
||||
}
|
||||
unset( $this->sessionDirtyTempTables[$table] );
|
||||
break;
|
||||
default:
|
||||
if ( isset( $this->sessionTempTables[$table] ) ) {
|
||||
$this->sessionTempTables[$table]['pristine'] = false;
|
||||
}
|
||||
$this->sessionDirtyTempTables[$table] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1189,28 +1133,24 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
protected function isPristineTemporaryTable( $table ) {
|
||||
$rawTable = $this->tableName( $table, 'raw' );
|
||||
|
||||
return isset( $this->sessionTempTables[$rawTable] )
|
||||
? $this->sessionTempTables[$rawTable]['pristine']
|
||||
: false;
|
||||
return (
|
||||
isset( $this->sessionTempTables[$rawTable] ) &&
|
||||
!isset( $this->sessionDirtyTempTables[$rawTable] )
|
||||
);
|
||||
}
|
||||
|
||||
public function query( $sql, $fname = __METHOD__, $flags = self::QUERY_NORMAL ) {
|
||||
$flags = (int)$flags; // b/c; this field used to be a bool
|
||||
// Double check that the SQL query is appropriate in the current context and is
|
||||
// allowed for an outside caller (e.g. does not break session/transaction tracking).
|
||||
// allowed for an outside caller (e.g. does not break transaction/session tracking).
|
||||
$this->assertQueryIsCurrentlyAllowed( $sql, $fname );
|
||||
|
||||
// Send the query to the server and fetch any corresponding errors
|
||||
list( $ret, $err, $errno, $errFlags ) = $this->executeQuery( $sql, $fname, $flags );
|
||||
list( $ret, $err, $errno, $unignorable ) = $this->executeQuery( $sql, $fname, $flags );
|
||||
if ( $ret === false ) {
|
||||
// An error occurred; log and report it as needed. Errors that corrupt the state of
|
||||
// the transaction/session cannot be silenced from the client.
|
||||
$ignore = (
|
||||
$this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS ) &&
|
||||
!$this->fieldHasBit( $errFlags, self::ERR_ABORT_SESSION ) &&
|
||||
!$this->fieldHasBit( $errFlags, self::ERR_ABORT_TRX )
|
||||
);
|
||||
$this->reportQueryError( $err, $errno, $sql, $fname, $ignore );
|
||||
$ignoreErrors = $this->fieldHasBit( $flags, self::QUERY_SILENCE_ERRORS );
|
||||
// Throw an error unless both the ignore flag was set and a rollback is not needed
|
||||
$this->reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
|
||||
}
|
||||
|
||||
return $ret;
|
||||
|
|
@ -1226,19 +1166,21 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
*
|
||||
* This is meant for internal use with Database subclasses.
|
||||
*
|
||||
* @param string $sql Original SQL statement query
|
||||
* @param string $sql Original SQL query
|
||||
* @param string $fname Name of the calling function
|
||||
* @param int $flags Bit field of class QUERY_* constants
|
||||
* @return array An n-tuple of:
|
||||
* - mixed|bool: An object, resource, or true on success; false on failure
|
||||
* - string: The result of calling lastError()
|
||||
* - int: The result of calling lastErrno()
|
||||
* - int: Bit field of ERR_* class constants
|
||||
* - bool: Whether a rollback is needed to allow future non-rollback queries
|
||||
* @throws DBUnexpectedError
|
||||
*/
|
||||
final protected function executeQuery( $sql, $fname, $flags ) {
|
||||
$this->assertHasConnectionHandle();
|
||||
|
||||
$priorTransaction = $this->trxLevel();
|
||||
|
||||
if ( $this->isWriteQuery( $sql, $flags ) ) {
|
||||
// Do not treat temporary table writes as "meaningful writes" since they are only
|
||||
// visible to one session and are not permanent. Profile them as reads. Integration
|
||||
|
|
@ -1249,16 +1191,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
foreach ( $tempTableChanges as list( $tmpType ) ) {
|
||||
$isPermWrite = $isPermWrite || ( $tmpType !== self::TEMP_NORMAL );
|
||||
}
|
||||
|
||||
// Permit temporary table writes on replica DB connections
|
||||
// but require a writable primary DB connection for any persistent writes.
|
||||
if ( $isPermWrite ) {
|
||||
$this->assertIsWritablePrimary();
|
||||
// DBConnRef uses QUERY_REPLICA_ROLE to enforce replica roles for raw SQL queries
|
||||
|
||||
// DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
|
||||
if ( $this->fieldHasBit( $flags, self::QUERY_REPLICA_ROLE ) ) {
|
||||
throw new DBReadOnlyRoleError(
|
||||
$this,
|
||||
"Cannot write; target role is DB_REPLICA"
|
||||
);
|
||||
throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1268,51 +1209,61 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
$tempTableChanges = [];
|
||||
}
|
||||
|
||||
// Add agent and calling method comments to the SQL
|
||||
$commentedSql = $this->getCommentedSql( $sql, $fname );
|
||||
// How many silent retry attempts are left for recoverable connection loss errors
|
||||
$retriesLeft = $this->fieldHasBit( $flags, self::QUERY_NO_RETRY ) ? 0 : 1;
|
||||
// Add trace comment to the begin of the sql string, right after the operator.
|
||||
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
|
||||
// NOTE: Don't add varying ids such as request id or session id to the comment.
|
||||
// It would break aggregation of similar queries in analysis tools (see T193050#7512149)
|
||||
$encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
|
||||
$commentedSql = preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
|
||||
|
||||
$corruptedTrx = false;
|
||||
|
||||
$cs = $this->commenceCriticalSection( __METHOD__ );
|
||||
|
||||
do {
|
||||
// Start a DBO_TRX wrapper transaction as needed (throw an error on failure)
|
||||
if ( $this->beginIfImplied( $sql, $fname, $flags ) ) {
|
||||
// Since begin() was called, any connection loss already handled
|
||||
--$retriesLeft;
|
||||
}
|
||||
// Send the query to the server, fetching any results and corresponding errors.
|
||||
// Silently retry the query if the error was just a recoverable connection loss.
|
||||
list( $ret, $err, $errno, $errflags ) = $this->executeQueryAttempt(
|
||||
$sql,
|
||||
$commentedSql,
|
||||
$isPermWrite,
|
||||
$fname,
|
||||
$flags
|
||||
);
|
||||
} while ( $this->fieldHasBit( $errflags, self::ERR_RETRY_QUERY ) && $retriesLeft-- > 0 );
|
||||
// Send the query to the server and fetch any corresponding errors.
|
||||
// This also doubles as a "ping" to see if the connection was dropped.
|
||||
list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
|
||||
$this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
|
||||
|
||||
// Check if the query failed due to a recoverable connection loss
|
||||
$allowRetry = !$this->fieldHasBit( $flags, self::QUERY_NO_RETRY );
|
||||
if ( $ret === false && $recoverableCL && $reconnected && $allowRetry ) {
|
||||
// Silently resend the query to the server since it is safe and possible
|
||||
list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
|
||||
$this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
|
||||
}
|
||||
|
||||
// Register creation and dropping of temporary tables
|
||||
$this->registerTempWrites( $ret, $tempTableChanges );
|
||||
|
||||
if ( $ret === false && $priorTransaction ) {
|
||||
if ( $recoverableSR ) {
|
||||
# We're ignoring an error that caused just the current query to be aborted.
|
||||
# But log the cause so we can log a deprecation notice if a caller actually
|
||||
# does ignore it.
|
||||
$this->transactionManager->setTrxStatusIgnoredCause( [ $err, $errno, $fname ] );
|
||||
} elseif ( !$recoverableCL ) {
|
||||
# Either the query was aborted or all queries after BEGIN where aborted.
|
||||
# In the first case, the only options going forward are (a) ROLLBACK, or
|
||||
# (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
|
||||
# option is ROLLBACK, since the snapshots would have been released.
|
||||
$corruptedTrx = true; // cannot recover
|
||||
$trxError = $this->getQueryException( $err, $errno, $sql, $fname );
|
||||
$this->transactionManager->setTransactionError( $trxError );
|
||||
}
|
||||
}
|
||||
|
||||
$this->completeCriticalSection( __METHOD__, $cs );
|
||||
|
||||
return [ $ret, $err, $errno, $errflags ];
|
||||
return [ $ret, $err, $errno, $corruptedTrx ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for doQuery() that handles a single SQL statement query attempt
|
||||
* Wrapper for doQuery() that handles DBO_TRX, profiling, logging, affected row count
|
||||
* tracking, and reconnects (without retry) on query failure due to connection loss
|
||||
*
|
||||
* This method handles profiling, debug logging, reconnection and the tracking of:
|
||||
* - write callers
|
||||
* - last write time
|
||||
* - affected row count of the last write
|
||||
* - whether writes occured in a transaction
|
||||
*
|
||||
* This method does *not* handle DBO_TRX transaction logic *nor* query retries.
|
||||
*
|
||||
* @param string $sql Original SQL statement query
|
||||
* @param string $commentedSql SQL statement query with debugging/trace comment
|
||||
* @param string $sql Original SQL query
|
||||
* @param string $commentedSql SQL query with debugging/trace comment
|
||||
* @param bool $isPermWrite Whether the query is a (non-temporary table) write
|
||||
* @param string $fname Name of the calling function
|
||||
* @param int $flags Bit field of class QUERY_* constants
|
||||
|
|
@ -1320,12 +1271,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
* - mixed|bool: An object, resource, or true on success; false on failure
|
||||
* - string: The result of calling lastError()
|
||||
* - int: The result of calling lastErrno()
|
||||
* - int: Bit field of ERR_* class constants
|
||||
* - bool: Whether a statement rollback error occurred
|
||||
* - bool: Whether a disconnect *both* happened *and* was recoverable
|
||||
* - bool: Whether a reconnection attempt was *both* made *and* succeeded
|
||||
* @throws DBUnexpectedError
|
||||
*/
|
||||
private function executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags ) {
|
||||
// Transaction attributes before issuing this query
|
||||
$priorSessInfo = $this->sessionStateInfo();
|
||||
$priorWritesPending = $this->writesOrCallbacksPending();
|
||||
|
||||
if ( ( $flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
|
||||
$this->beginIfImplied( $sql, $fname );
|
||||
}
|
||||
|
||||
// Keep track of whether the transaction has write queries pending
|
||||
if ( $isPermWrite ) {
|
||||
|
|
@ -1336,25 +1292,29 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
);
|
||||
}
|
||||
|
||||
$prefix = ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) ? 'query-m: ' : 'query: ';
|
||||
$generalizedSql = new GeneralizedSql( $sql, $prefix );
|
||||
$prefix = $this->topologyRole === IDatabase::ROLE_STREAMING_MASTER ? 'query-m: ' : 'query: ';
|
||||
$generalizedSql = new GeneralizedSql( $commentedSql, $prefix );
|
||||
|
||||
$startTime = microtime( true );
|
||||
$ps = $this->profiler ? ( $this->profiler )( $generalizedSql->stringify() ) : null;
|
||||
$ps = $this->profiler
|
||||
? ( $this->profiler )( $generalizedSql->stringify() )
|
||||
: null;
|
||||
$this->affectedRowCount = null;
|
||||
$this->lastQuery = $sql;
|
||||
$ret = $this->doQuery( $commentedSql );
|
||||
$lastError = $this->lastError();
|
||||
$lastErrno = $this->lastErrno();
|
||||
|
||||
$this->affectedRowCount = $this->affectedRows();
|
||||
unset( $ps ); // profile out (if set)
|
||||
$queryRuntime = max( microtime( true ) - $startTime, 0.0 );
|
||||
|
||||
$errflags = self::ERR_NONE;
|
||||
$recoverableSR = false; // recoverable statement rollback?
|
||||
$recoverableCL = false; // recoverable connection loss?
|
||||
$reconnected = false; // reconnection both attempted and succeeded?
|
||||
|
||||
if ( $ret !== false ) {
|
||||
$this->lastPing = $startTime;
|
||||
// Keep track of whether the transaction has write queries pending
|
||||
if ( $isPermWrite && $this->trxLevel() ) {
|
||||
$this->transactionManager->updateTrxWriteQueryReport(
|
||||
$this->getQueryVerb( $sql ),
|
||||
|
|
@ -1364,51 +1324,13 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
);
|
||||
}
|
||||
} elseif ( $this->isConnectionError( $lastErrno ) ) {
|
||||
// Connection lost before or during the query...
|
||||
// Determine how to proceed given the lost session state
|
||||
$connLossFlag = $this->assessConnectionLoss( $sql, $queryRuntime, $priorSessInfo );
|
||||
// Update session state tracking and try to reestablish a connection
|
||||
# Check if no meaningful session state was lost
|
||||
$recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
|
||||
# Update session state tracking and try to restore the connection
|
||||
$reconnected = $this->replaceLostConnection( $lastErrno, __METHOD__ );
|
||||
// Check if important server-side session-level state was lost
|
||||
if ( $connLossFlag >= self::ERR_ABORT_SESSION ) {
|
||||
$sessionError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
|
||||
$this->transactionManager->setSessionError( $sessionError );
|
||||
}
|
||||
// Check if important server-side transaction-level state was lost
|
||||
if ( $connLossFlag >= self::ERR_ABORT_TRX ) {
|
||||
$trxError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
|
||||
$this->transactionManager->setTransactionError( $trxError );
|
||||
}
|
||||
// Check if the query should be retried (having made the reconnection attempt)
|
||||
if ( $connLossFlag === self::ERR_RETRY_QUERY ) {
|
||||
$errflags |= ( $reconnected ? self::ERR_RETRY_QUERY : self::ERR_ABORT_QUERY );
|
||||
} else {
|
||||
$errflags |= $connLossFlag;
|
||||
}
|
||||
} elseif ( $this->isKnownStatementRollbackError( $lastErrno ) ) {
|
||||
// Query error triggered a server-side statement-only rollback...
|
||||
$errflags |= self::ERR_ABORT_QUERY;
|
||||
if ( $this->trxLevel() ) {
|
||||
// Allow legacy callers to ignore such errors via QUERY_IGNORE_DBO_TRX and
|
||||
// try/catch. However, a deprecation notice will be logged on the next query.
|
||||
$cause = [ $lastError, $lastErrno, $fname ];
|
||||
$this->transactionManager->setTrxStatusIgnoredCause( $cause );
|
||||
}
|
||||
} else {
|
||||
// Some other error occurred during the query...
|
||||
if ( $this->trxLevel() ) {
|
||||
// Server-side handling of errors during transactions varies widely depending on
|
||||
// the RDBMS type and configuration. There are several possible results: (a) the
|
||||
// whole transaction is rolled back, (b) only the queries after BEGIN are rolled
|
||||
// back, (c) the transaction is marked as "aborted" and a ROLLBACK is required
|
||||
// before other queries are permitted. For compatibility reasons, pessimistically
|
||||
// require a ROLLBACK query (not using SAVEPOINT) before allowing other queries.
|
||||
$trxError = $this->getQueryException( $lastError, $lastErrno, $sql, $fname );
|
||||
$this->transactionManager->setTransactionError( $trxError );
|
||||
$errflags |= self::ERR_ABORT_TRX;
|
||||
} else {
|
||||
$errflags |= self::ERR_ABORT_QUERY;
|
||||
}
|
||||
# Check if only the last query was rolled back
|
||||
$recoverableSR = $this->wasKnownStatementRollbackError();
|
||||
}
|
||||
|
||||
if ( $sql === self::PING_QUERY ) {
|
||||
|
|
@ -1447,22 +1369,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
static::class . '::doQuery() should return an IResultWrapper' );
|
||||
}
|
||||
|
||||
return [ $ret, $lastError, $lastErrno, $errflags ];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sql
|
||||
* @param string $fname
|
||||
* @return string
|
||||
*/
|
||||
private function getCommentedSql( $sql, $fname ) {
|
||||
// Add trace comment to the begin of the sql string, right after the operator.
|
||||
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
|
||||
// NOTE: Don't add varying ids such as request id or session id to the comment.
|
||||
// It would break aggregation of similar queries in analysis tools (see T193050#7512149)
|
||||
$encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
|
||||
|
||||
return preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
|
||||
return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1470,24 +1377,16 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
*
|
||||
* @param string $sql
|
||||
* @param string $fname
|
||||
* @param int $flags
|
||||
* @return bool Whether an implicit transaction was started
|
||||
* @throws DBError
|
||||
*/
|
||||
private function beginIfImplied( $sql, $fname, $flags ) {
|
||||
private function beginIfImplied( $sql, $fname ) {
|
||||
if (
|
||||
!$this->fieldHasBit( $flags, self::QUERY_IGNORE_DBO_TRX ) &&
|
||||
!$this->trxLevel() &&
|
||||
$this->getFlag( self::DBO_TRX ) &&
|
||||
$this->isTransactableQuery( $sql )
|
||||
) {
|
||||
$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
|
||||
$this->transactionManager->turnOnAutomatic();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1500,119 +1399,60 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
*/
|
||||
private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
|
||||
$verb = $this->getQueryVerb( $sql );
|
||||
|
||||
if ( $verb === 'USE' ) {
|
||||
throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
|
||||
}
|
||||
|
||||
if ( $verb === 'ROLLBACK' ) {
|
||||
// Whole transaction rollback is used for recovery
|
||||
if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->csmError ) {
|
||||
throw new DBTransactionStateError(
|
||||
$this,
|
||||
"Cannot execute query from $fname while session state is out of sync",
|
||||
[],
|
||||
$this->csmError
|
||||
"Cannot execute query from $fname while session state is out of sync.\n\n" .
|
||||
$this->csmError->getMessage() . "\n" .
|
||||
$this->csmError->getTraceAsString()
|
||||
);
|
||||
}
|
||||
|
||||
$this->transactionManager->assertSessionStatus( $this, $fname );
|
||||
|
||||
if ( $verb !== 'ROLLBACK TO SAVEPOINT' ) {
|
||||
$this->transactionManager->assertTransactionStatus(
|
||||
$this,
|
||||
$this->deprecationLogger,
|
||||
$fname
|
||||
);
|
||||
}
|
||||
$this->transactionManager->assertTransactionStatus( $this, $this->deprecationLogger, $fname );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine how to handle a connection lost discovered during a query attempt
|
||||
* Determine whether it is safe to retry queries after a database connection is lost
|
||||
*
|
||||
* This checks if explicit transactions, pending transaction writes, and important
|
||||
* session-level state (locks, temp tables) was lost. Point-in-time read snapshot loss
|
||||
* is considered acceptable for DBO_TRX logic.
|
||||
*
|
||||
* If state was lost, but that loss was discovered during a ROLLBACK that would have
|
||||
* destroyed that state anyway, treat the error as recoverable.
|
||||
*
|
||||
* @param string $sql SQL query statement that encountered or caused the connection loss
|
||||
* @param float $walltime How many seconds passes while attempting the query
|
||||
* @param array $priorSessInfo Result of sessionStateInfo(), just before the query
|
||||
* @return int Recovery approach. One of the following ERR_* class constants:
|
||||
* - Database::ERR_RETRY_QUERY: reconnect silently, retry query
|
||||
* - Database::ERR_ABORT_QUERY: reconnect silently, do not retry query
|
||||
* - Database::ERR_ABORT_TRX: reconnect, throw error, enforce transaction rollback
|
||||
* - Database::ERR_ABORT_SESSION: reconnect, throw error, enforce session rollback
|
||||
* @param string $sql SQL query
|
||||
* @param bool $priorWritesPending Whether there is a transaction open with
|
||||
* possible write queries or transaction pre-commit/idle callbacks
|
||||
* waiting on it to finish.
|
||||
* @return bool True if it is safe to retry the query, false otherwise
|
||||
*/
|
||||
private function assessConnectionLoss( string $sql, float $walltime, array $priorSessInfo ) {
|
||||
$verb = $this->getQueryVerb( $sql );
|
||||
|
||||
if ( $walltime < self::DROPPED_CONN_BLAME_THRESHOLD_SEC ) {
|
||||
// Query failed quickly; the connection was probably lost before the query was sent
|
||||
$res = self::ERR_RETRY_QUERY;
|
||||
} else {
|
||||
// Query took a long time; the connection was probably lost during query execution
|
||||
$res = self::ERR_ABORT_QUERY;
|
||||
}
|
||||
|
||||
// List of problems causing session/transaction state corruption
|
||||
private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
|
||||
$blockers = [];
|
||||
// Loss of named locks breaks future callers relying on those locks for critical sections
|
||||
foreach ( $priorSessInfo['sessNamedLocks'] as $lockName => $lockInfo ) {
|
||||
if ( $lockInfo['trxId'] && $lockInfo['trxId'] === $priorSessInfo['trxId'] ) {
|
||||
// Treat lost locks acquired during the lost transaction as a transaction state
|
||||
// problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
|
||||
// rollback automatically triggered server-side.
|
||||
if ( $verb !== 'ROLLBACK' ) {
|
||||
$res = max( $res, self::ERR_ABORT_TRX );
|
||||
$blockers[] = "named lock '$lockName'";
|
||||
}
|
||||
} else {
|
||||
// Treat lost locks acquired either during prior transactions or during no
|
||||
// transaction as a session state problem.
|
||||
$res = max( $res, self::ERR_ABORT_SESSION );
|
||||
$blockers[] = "named lock '$lockName'";
|
||||
}
|
||||
if ( $this->sessionNamedLocks ) {
|
||||
// Named locks were automatically released, breaking the expectations
|
||||
// of callers relying on those locks for critical section enforcement
|
||||
$blockers[] = 'named locks';
|
||||
}
|
||||
// Loss of temp tables breaks future callers relying on those tables for queries
|
||||
foreach ( $priorSessInfo['sessTempTables'] as $tableName => $tableInfo ) {
|
||||
if ( $tableInfo['trxId'] && $tableInfo['trxId'] === $priorSessInfo['trxId'] ) {
|
||||
// Treat lost temp tables created during the lost transaction as a transaction
|
||||
// state problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
|
||||
// rollback automatically triggered server-side.
|
||||
if ( $verb !== 'ROLLBACK' ) {
|
||||
$res = max( $res, self::ERR_ABORT_TRX );
|
||||
$blockers[] = "temp table '$tableName'";
|
||||
}
|
||||
} else {
|
||||
// Treat lost temp tables created either during prior transactions or during
|
||||
// no transaction as a session state problem.
|
||||
$res = max( $res, self::ERR_ABORT_SESSION );
|
||||
$blockers[] = "temp table '$tableName'";
|
||||
}
|
||||
if ( $this->sessionTempTables ) {
|
||||
// Temp tables were automatically dropped, breaking the expectations
|
||||
// of callers relying on those tables having been created/populated
|
||||
$blockers[] = 'temp tables';
|
||||
}
|
||||
// Loss of transaction writes breaks future callers and DBO_TRX logic relying on those
|
||||
// writes to be atomic and still pending. Connection loss on ROLLBACK (non-SAVEPOINT) is
|
||||
// tolerable since rollback automatically triggered server-side.
|
||||
if ( $priorSessInfo['trxWriteCallers'] && $verb !== 'ROLLBACK' ) {
|
||||
$res = max( $res, self::ERR_ABORT_TRX );
|
||||
$blockers[] = 'uncommitted writes';
|
||||
if ( $priorWritesPending && $sql !== 'ROLLBACK' ) {
|
||||
// Transaction was automatically rolled back, breaking the expectations
|
||||
// of callers and DBO_TRX semantics relying on that transaction to provide
|
||||
// atomic writes (point-in-time snapshot loss is acceptable for DBO_TRX)
|
||||
$blockers[] = 'transaction writes';
|
||||
}
|
||||
if ( $priorSessInfo['trxPreCommitCbCallers'] && $verb !== 'ROLLBACK' ) {
|
||||
$res = max( $res, self::ERR_ABORT_TRX );
|
||||
$blockers[] = 'pre-commit callbacks';
|
||||
}
|
||||
if ( $priorSessInfo['trxExplicit'] && $verb !== 'ROLLBACK' && $sql !== 'COMMIT' ) {
|
||||
// Transaction automatically rolled back, breaking the expectations of callers
|
||||
// relying on that transaction to provide atomic writes, serializability, or use
|
||||
// one point-in-time snapshot for all reads. Assume that connection loss is OK
|
||||
// with ROLLBACK (non-SAVEPOINT). Likewise for COMMIT (T127428).
|
||||
$res = max( $res, self::ERR_ABORT_TRX );
|
||||
if ( $this->transactionManager->explicitTrxActive() && $sql !== 'ROLLBACK' && $sql !== 'COMMIT' ) {
|
||||
// Transaction was automatically rolled back, breaking the expectations of
|
||||
// callers relying on that transaction to provide atomic writes, serializability,
|
||||
// or read results consistent with a single point-in-time snapshot. Disconnection
|
||||
// on ROLLBACK is not an issue, since the intended result of rolling back the
|
||||
// transaction was in fact achieved. Disconnection on COMMIT of an empty transaction
|
||||
// is also not an issue, for similar reasons (T127428).
|
||||
$blockers[] = 'explicit transaction';
|
||||
}
|
||||
|
||||
|
|
@ -1625,9 +1465,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
'db_log_category' => 'connection'
|
||||
] )
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $res;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1638,6 +1480,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
// https://dev.mysql.com/doc/refman/5.7/en/implicit-commit.html
|
||||
// https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
|
||||
$this->sessionTempTables = [];
|
||||
$this->sessionDirtyTempTables = [];
|
||||
// https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
|
||||
// https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
|
||||
$this->sessionNamedLocks = [];
|
||||
|
|
@ -1664,11 +1507,25 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
// Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
|
||||
// If callback suppression is set then the array will remain unhandled.
|
||||
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
|
||||
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener().
|
||||
// If callback suppression is set then the array will remain unhandled.
|
||||
// Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener()
|
||||
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cause of the error is detected to be a timeout.
|
||||
*
|
||||
* It returns false by default, and not all engines support detecting this yet.
|
||||
* If this returns false, it will be treated as a generic query error.
|
||||
*
|
||||
* @stable to override
|
||||
* @param string $error Error text
|
||||
* @param int $errno Error number
|
||||
* @return bool
|
||||
*/
|
||||
protected function wasQueryTimeout( $error, $errno ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a query error. Log the error, and if neither the object ignore
|
||||
* flag nor the $ignoreErrors flag is set, throw a DBQueryError.
|
||||
|
|
@ -1721,7 +1578,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
* @return DBError
|
||||
*/
|
||||
private function getQueryException( $error, $errno, $sql, $fname ) {
|
||||
if ( $this->isQueryTimeoutError( $errno ) ) {
|
||||
if ( $this->wasQueryTimeout( $error, $errno ) ) {
|
||||
return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
|
||||
} elseif ( $this->isConnectionError( $errno ) ) {
|
||||
return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
|
||||
|
|
@ -4040,35 +3897,19 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
* @stable to override
|
||||
* @param int|string $errno
|
||||
* @return bool Whether the given query error was a connection drop
|
||||
* @since 1.38
|
||||
*/
|
||||
protected function isConnectionError( $errno ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cause of the error is detected to be a timeout.
|
||||
*
|
||||
* It returns false by default, and not all engines support detecting this yet.
|
||||
* If this returns false, it will be treated as a generic query error.
|
||||
*
|
||||
* @stable to override
|
||||
* @param int $errno Error number
|
||||
* @return bool
|
||||
*/
|
||||
protected function isQueryTimeoutError( $errno ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @stable to override
|
||||
* @param int|string $errno
|
||||
* @return bool Whether it is known that the last query error only caused statement rollback
|
||||
* @note This is for backwards compatibility for callers catching DBError exceptions in
|
||||
* order to ignore problems like duplicate key errors or foreign key violations
|
||||
* @since 1.31
|
||||
*/
|
||||
protected function isKnownStatementRollbackError( $errno ) {
|
||||
protected function wasKnownStatementRollbackError() {
|
||||
return false; // don't know; it could have caused a transaction rollback
|
||||
}
|
||||
|
||||
|
|
@ -4540,12 +4381,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
);
|
||||
}
|
||||
} else {
|
||||
// Put the transaction into an error state if it's not already in one
|
||||
$trxError = new DBUnexpectedError(
|
||||
$this,
|
||||
"Uncancelable atomic section canceled (got $fname)"
|
||||
);
|
||||
$this->transactionManager->setTransactionError( $trxError );
|
||||
$this->transactionManager->setTransactionErrorFromStatus( $this, $fname );
|
||||
}
|
||||
} finally {
|
||||
// Fix up callbacks owned by the sections that were just cancelled.
|
||||
|
|
@ -4690,15 +4526,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
}
|
||||
|
||||
if ( !$this->trxLevel() ) {
|
||||
$this->transactionManager->setTrxStatusToNone();
|
||||
$this->transactionManager->clearPreEndCallbacks();
|
||||
if ( $this->transactionManager->trxLevel() <= TransactionManager::STATUS_TRX_ERROR ) {
|
||||
$this->connLogger->info(
|
||||
"$fname: acknowledged server-side transaction loss on {db_server}",
|
||||
$this->getLogContext()
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -4739,53 +4567,6 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
$this->transactionManager = $transactionManager;
|
||||
}
|
||||
|
||||
public function flushSession( $fname = __METHOD__, $owner = null ) {
|
||||
if ( $this->ownerId !== null && $owner !== $this->ownerId ) {
|
||||
throw new DBUnexpectedError(
|
||||
$this,
|
||||
"$fname: Database is owned by ID '{$this->ownerId}' (got '$owner')"
|
||||
);
|
||||
}
|
||||
|
||||
if ( $this->trxLevel() ) {
|
||||
// Any existing transaction should have been rolled back already
|
||||
throw new DBUnexpectedError(
|
||||
$this,
|
||||
"$fname: transaction still in progress (not yet rolled back)"
|
||||
);
|
||||
}
|
||||
|
||||
// If the session state was already lost due to either an unacknowledged session
|
||||
// state loss error (e.g. dropped connection) or an explicit connection close call,
|
||||
// then there is nothing to do here. Note that such cases, even temporary tables and
|
||||
// server-side config variables are lost (the invocation of this method is assumed to
|
||||
// imply that such losses are tolerable).
|
||||
if ( $this->transactionManager->sessionStatus() <= TransactionManager::STATUS_SESS_ERROR ) {
|
||||
$this->connLogger->info(
|
||||
"$fname: acknowledged server-side session loss on {db_server}",
|
||||
$this->getLogContext()
|
||||
);
|
||||
} elseif ( $this->isOpen() ) {
|
||||
// Connection handle exists; server-side session state must be flushed
|
||||
$this->doFlushSession( $fname );
|
||||
$this->sessionNamedLocks = [];
|
||||
}
|
||||
|
||||
$this->transactionManager->clearSessionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the server-side session state for named locks and table locks
|
||||
*
|
||||
* Connection and query errors will be suppressed and logged
|
||||
*
|
||||
* @param string $fname
|
||||
* @since 1.38
|
||||
*/
|
||||
protected function doFlushSession( $fname ) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
|
||||
$this->transactionManager->onFlushSnapshot( $this, $fname, $flush, $this->getTransactionRoundId() );
|
||||
$this->commit( $fname, self::FLUSHING_INTERNAL );
|
||||
|
|
@ -4933,15 +4714,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
/**
|
||||
* Get the replica DB lag when the current transaction started
|
||||
*
|
||||
* This is useful given that transactions might use point-in-time read snapshots,
|
||||
* in which case the lag estimate should be recorded just before the transaction
|
||||
* establishes the read snapshot (either BEGIN or the first SELECT/write query).
|
||||
* This is useful when transactions might use snapshot isolation
|
||||
* (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
|
||||
* is this lag plus transaction duration. If they don't, it is still
|
||||
* safe to be pessimistic. This returns null if there is no transaction.
|
||||
*
|
||||
* If snapshots are not used, it is still safe to be pessimistic.
|
||||
* This returns null if the lag status for this transaction was not yet recorded.
|
||||
*
|
||||
* This returns null if there is no transaction or the lag status was not yet recorded.
|
||||
*
|
||||
* @return array|null ('lag': seconds or false, 'since': UNIX timestamp of BEGIN) or null
|
||||
* @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
|
||||
* @since 1.27
|
||||
*/
|
||||
final protected function getRecordedTransactionLagStatus() {
|
||||
|
|
@ -5307,19 +5087,14 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
$lockTsUnix = $this->doLock( $lockName, $method, $timeout );
|
||||
if ( $lockTsUnix !== null ) {
|
||||
$locked = true;
|
||||
$this->sessionNamedLocks[$lockName] = [
|
||||
'ts' => $lockTsUnix,
|
||||
'trxId' => $this->transactionManager->getTrxId()
|
||||
];
|
||||
$this->sessionNamedLocks[$lockName] = $lockTsUnix;
|
||||
} else {
|
||||
$locked = false;
|
||||
$this->queryLogger->info(
|
||||
__METHOD__ . " failed to acquire lock '{lockname}'",
|
||||
$this->queryLogger->info( __METHOD__ . " failed to acquire lock '{lockname}'",
|
||||
[
|
||||
'lockname' => $lockName,
|
||||
'db_log_category' => 'locking'
|
||||
]
|
||||
);
|
||||
] );
|
||||
}
|
||||
|
||||
if ( $this->fieldHasBit( $flags, self::LOCK_TIMESTAMP ) ) {
|
||||
|
|
@ -5629,13 +5404,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
|
|||
* try {
|
||||
* //...send a query that changes the session/transaction state...
|
||||
* } catch ( DBError $e ) {
|
||||
* // Rely on assertQueryIsCurrentlyAllowed()/canRecoverFromDisconnect() to ensure
|
||||
* // the rollback of incomplete transactions and the prohibition of reconnections
|
||||
* // that mask a loss of session state (e.g. named locks and temp tables)
|
||||
* $this->completeCriticalSection( __METHOD__, $cs );
|
||||
* throw $expectedException;
|
||||
* }
|
||||
* try {
|
||||
* //...send another query that changes the session/transaction state...
|
||||
* } catch ( DBError $trxError ) {
|
||||
* // Require ROLLBACK before allowing any other queries from outside callers
|
||||
* // Inform assertQueryIsCurrentlyAllowed() that the transaction must be rolled
|
||||
* // back (e.g. even if the error was a pre-query check or normally recoverable)
|
||||
* $this->completeCriticalSection( __METHOD__, $cs, $trxError );
|
||||
* throw $expectedException;
|
||||
* }
|
||||
|
|
|
|||
|
|
@ -170,12 +170,11 @@ abstract class DatabaseMysqlBase extends Database {
|
|||
// @phan-suppress-next-next-line PhanRedundantCondition
|
||||
// If kept for safety and to avoid broken query
|
||||
if ( $set ) {
|
||||
$sql = 'SET ' . implode( ', ', $set );
|
||||
$flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
|
||||
list( $ret, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
|
||||
if ( $ret === false ) {
|
||||
$this->reportQueryError( $err, $errno, $sql, __METHOD__ );
|
||||
}
|
||||
$this->query(
|
||||
'SET ' . implode( ', ', $set ),
|
||||
__METHOD__,
|
||||
self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX
|
||||
);
|
||||
}
|
||||
} catch ( RuntimeException $e ) {
|
||||
throw $this->newExceptionAfterConnectError( $e->getMessage() );
|
||||
|
|
@ -261,6 +260,12 @@ abstract class DatabaseMysqlBase extends Database {
|
|||
*/
|
||||
abstract protected function mysqlError( $conn = null );
|
||||
|
||||
protected function wasQueryTimeout( $error, $errno ) {
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
|
||||
// https://phabricator.wikimedia.org/T170638
|
||||
return in_array( $errno, [ 2062, 3024 ] );
|
||||
}
|
||||
|
||||
protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
|
||||
$row = $this->getReplicationSafetyInfo();
|
||||
// For row-based-replication, the resulting changes will be relayed, not the query
|
||||
|
|
@ -1114,24 +1119,6 @@ abstract class DatabaseMysqlBase extends Database {
|
|||
return true;
|
||||
}
|
||||
|
||||
protected function doFlushSession( $fname ) {
|
||||
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS | self::QUERY_NO_RETRY;
|
||||
|
||||
// In MySQL, ROLLBACK does not automatically release table locks;
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
|
||||
$sql = "UNLOCK TABLES";
|
||||
list( $res, $err, $errno ) = $this->executeQuery( $sql, $fname, $flags );
|
||||
if ( $res === false ) {
|
||||
$this->reportQueryError( $err, $errno, $sql, $fname, true );
|
||||
}
|
||||
|
||||
$sql = "RELEASE_ALL_LOCKS()";
|
||||
list( $res, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
|
||||
if ( $res === false ) {
|
||||
$this->reportQueryError( $err, $errno, $sql, $fname, true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $value
|
||||
*/
|
||||
|
|
@ -1239,20 +1226,12 @@ abstract class DatabaseMysqlBase extends Database {
|
|||
}
|
||||
|
||||
protected function isConnectionError( $errno ) {
|
||||
// https://mariadb.com/kb/en/mariadb-error-codes/
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
|
||||
return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
|
||||
return $errno == 2013 || $errno == 2006;
|
||||
}
|
||||
|
||||
protected function isQueryTimeoutError( $errno ) {
|
||||
// https://mariadb.com/kb/en/mariadb-error-codes/
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
|
||||
return in_array( $errno, [ 3024, 1969 ], true );
|
||||
}
|
||||
protected function wasKnownStatementRollbackError() {
|
||||
$errno = $this->lastErrno();
|
||||
|
||||
protected function isKnownStatementRollbackError( $errno ) {
|
||||
// https://mariadb.com/kb/en/mariadb-error-codes/
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
|
||||
if ( $errno === 1205 ) { // lock wait timeout
|
||||
// Note that this is uncached to avoid stale values of SET is used
|
||||
$res = $this->query(
|
||||
|
|
@ -1265,7 +1244,9 @@ abstract class DatabaseMysqlBase extends Database {
|
|||
// https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
|
||||
return ( $row && !$row->Value );
|
||||
}
|
||||
return in_array( $errno, [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
|
||||
|
||||
// See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
|
||||
return in_array( $errno, [ 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -661,12 +661,7 @@ __INDEXATTR__;
|
|||
return in_array( $errno, $codes, true );
|
||||
}
|
||||
|
||||
protected function isQueryTimeoutError( $errno ) {
|
||||
// https://www.postgresql.org/docs/9.2/static/errcodes-appendix.html
|
||||
return ( $errno === '57014' );
|
||||
}
|
||||
|
||||
protected function isKnownStatementRollbackError( $errno ) {
|
||||
protected function wasKnownStatementRollbackError() {
|
||||
return false; // transaction has to be rolled-back from error state
|
||||
}
|
||||
|
||||
|
|
@ -1334,20 +1329,6 @@ SQL;
|
|||
return ( $row->released === 't' );
|
||||
}
|
||||
|
||||
protected function doFlushSession( $fname ) {
|
||||
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS | self::QUERY_NO_RETRY;
|
||||
|
||||
// In Postgres, ROLLBACK already releases table locks;
|
||||
// https://www.postgresql.org/docs/9.4/sql-lock.html
|
||||
|
||||
// https://www.postgresql.org/docs/9.1/functions-admin.html
|
||||
$sql = "pg_advisory_unlock_all()";
|
||||
list( $res, $err, $errno ) = $this->executeQuery( $sql, __METHOD__, $flags );
|
||||
if ( $res === false ) {
|
||||
$this->reportQueryError( $err, $errno, $sql, $fname, true );
|
||||
}
|
||||
}
|
||||
|
||||
public function serverIsReadOnly() {
|
||||
$res = $this->query(
|
||||
"SHOW default_transaction_read_only",
|
||||
|
|
|
|||
|
|
@ -83,11 +83,20 @@ class DatabaseSqlite extends Database {
|
|||
} elseif ( isset( $params['dbDirectory'] ) ) {
|
||||
$this->dbDir = $params['dbDirectory'];
|
||||
}
|
||||
|
||||
parent::__construct( $params );
|
||||
|
||||
$this->trxMode = strtoupper( $params['trxMode'] ?? '' );
|
||||
|
||||
$this->initLockManager();
|
||||
$lockDirectory = $this->getLockFileDirectory();
|
||||
if ( $lockDirectory !== null ) {
|
||||
$this->lockMgr = new FSLockManager( [
|
||||
'domain' => $this->getDomainID(),
|
||||
'lockDirectory' => $lockDirectory
|
||||
] );
|
||||
} else {
|
||||
$this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
|
||||
}
|
||||
}
|
||||
|
||||
protected static function getAttributes() {
|
||||
|
|
@ -224,29 +233,12 @@ class DatabaseSqlite extends Database {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize/reset the lock manager instance
|
||||
*/
|
||||
private function initLockManager() {
|
||||
$lockDirectory = $this->getLockFileDirectory();
|
||||
if ( $lockDirectory !== null ) {
|
||||
$this->lockMgr = new FSLockManager( [
|
||||
'domain' => $this->getDomainID(),
|
||||
'lockDirectory' => $lockDirectory
|
||||
] );
|
||||
} else {
|
||||
$this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does not actually close the connection, just destroys the reference for GC to do its work
|
||||
* @return bool
|
||||
*/
|
||||
protected function closeConnection() {
|
||||
$this->conn = null;
|
||||
// Release all locks, via FSLockManager::__destruct, as the baset class expects
|
||||
$this->lockMgr = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -628,7 +620,7 @@ class DatabaseSqlite extends Database {
|
|||
return $errno == 17; // SQLITE_SCHEMA;
|
||||
}
|
||||
|
||||
protected function isKnownStatementRollbackError( $errno ) {
|
||||
protected function wasKnownStatementRollbackError() {
|
||||
// ON CONFLICT ROLLBACK clauses make it so that SQLITE_CONSTRAINT error is
|
||||
// ambiguous with regard to whether it implies a ROLLBACK or an ABORT happened.
|
||||
// https://sqlite.org/lang_createtable.html#uniqueconst
|
||||
|
|
@ -1080,17 +1072,6 @@ class DatabaseSqlite extends Database {
|
|||
|
||||
protected function doHandleSessionLossPreconnect() {
|
||||
$this->sessionAttachedDbs = [];
|
||||
// Release all locks, via FSLockManager::__destruct, as the baset class expects
|
||||
$this->lockMgr = null;
|
||||
// Create a new lock manager instance
|
||||
$this->initLockManager();
|
||||
}
|
||||
|
||||
protected function doFlushSession( $fname ) {
|
||||
// Release all locks, via FSLockManager::__destruct, as the baset class expects
|
||||
$this->lockMgr = null;
|
||||
// Create a new lock manager instance
|
||||
$this->initLockManager();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1977,30 +1977,14 @@ interface IDatabase {
|
|||
*
|
||||
* @param string $fname Calling function name
|
||||
* @param string $flush Flush flag, set to a situationally valid IDatabase::FLUSHING_*
|
||||
* constant to disable warnings about explicitly rolling back implicit transactions.
|
||||
* This will silently break any ongoing explicit transaction. Only set the flush flag
|
||||
* if you are sure that it is safe to ignore these warnings in your context.
|
||||
* constant to disable warnings about calling rollback when no transaction is in
|
||||
* progress. This will silently break any ongoing explicit transaction. Only set the
|
||||
* flush flag if you are sure that it is safe to ignore these warnings in your context.
|
||||
* @throws DBError If an error occurs, {@see query}
|
||||
* @since 1.23 Added $flush parameter
|
||||
*/
|
||||
public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE );
|
||||
|
||||
/**
|
||||
* Release important session-level state (named lock, table locks) as post-rollback cleanup
|
||||
*
|
||||
* This should only be called by a load balancer or if the handle is not attached to one.
|
||||
* Also, there must be no chance that a future caller will still be expecting some of the
|
||||
* lost session state.
|
||||
*
|
||||
* Connection and query errors will be suppressed and logged
|
||||
*
|
||||
* @param string $fname Calling function name
|
||||
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
|
||||
* @throws DBError If an error occurs, {@see query}
|
||||
* @since 1.38
|
||||
*/
|
||||
public function flushSession( $fname = __METHOD__, $owner = null );
|
||||
|
||||
/**
|
||||
* Commit any transaction but error out if writes or callbacks are pending
|
||||
*
|
||||
|
|
|
|||
|
|
@ -225,11 +225,12 @@ interface IMaintainableDatabase extends IDatabase {
|
|||
);
|
||||
|
||||
/**
|
||||
* Check if lockTables() locks are transaction-level locks instead of session-level
|
||||
* Checks if table locks acquired by lockTables() are transaction-bound in their scope
|
||||
*
|
||||
* Transaction-level table locks can only be acquired within a transaction and get released
|
||||
* when that transaction terminates. Session-level table locks are acquired outside of any
|
||||
* transaction and are incompatible with transactions.
|
||||
* Transaction-bound table locks will be released when the current transaction terminates.
|
||||
* Table locks that are not bound to a transaction are not effected by BEGIN/COMMIT/ROLLBACK
|
||||
* and will last until either lockTables()/unlockTables() is called or the TCP connection to
|
||||
* the database is closed.
|
||||
*
|
||||
* @return bool
|
||||
* @since 1.29
|
||||
|
|
|
|||
|
|
@ -40,11 +40,6 @@ class TransactionManager {
|
|||
/** @var int No transaction is active */
|
||||
public const STATUS_TRX_NONE = 3;
|
||||
|
||||
/** Session is in a error state requiring a reset */
|
||||
public const STATUS_SESS_ERROR = 1;
|
||||
/** Session is in a normal state */
|
||||
public const STATUS_SESS_OK = 2;
|
||||
|
||||
/** @var float Guess of how many seconds it takes to replicate a small insert */
|
||||
private const TINY_WRITE_SEC = 0.010;
|
||||
/** @var float Consider a write slow if it took more than this many seconds */
|
||||
|
|
@ -61,14 +56,11 @@ class TransactionManager {
|
|||
private $trxTimestamp = null;
|
||||
/** @var int Transaction status */
|
||||
private $trxStatus = self::STATUS_TRX_NONE;
|
||||
/** @var Throwable|null The cause of any unresolved transaction state error, or, null */
|
||||
/** @var Throwable|null The last error that caused the status to become STATUS_TRX_ERROR */
|
||||
private $trxStatusCause;
|
||||
/** @var array|null Details of the last statement-rollback error for the last transaction */
|
||||
/** @var array|null Error details of the last statement-only rollback */
|
||||
private $trxStatusIgnoredCause;
|
||||
|
||||
/** @var Throwable|null The cause of any unresolved session state loss error, or, null */
|
||||
private $sessionError;
|
||||
|
||||
/** @var string[] Write query callers of the current transaction */
|
||||
private $trxWriteCallers = [];
|
||||
/** @var float Seconds spent in write queries for the current transaction */
|
||||
|
|
@ -126,13 +118,6 @@ class TransactionManager {
|
|||
return ( $this->trxId != '' ) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTrxId(): string {
|
||||
return $this->trxId;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: This should be removed once all usages have been migrated here
|
||||
* @param string $mode One of IDatabase::TRANSACTION_* values
|
||||
|
|
@ -143,7 +128,6 @@ class TransactionManager {
|
|||
$nextTrxId = ( $nextTrxId !== null ? $nextTrxId++ : mt_rand() ) % 0xffff;
|
||||
$this->trxId = sprintf( '%06x', mt_rand( 0, 0xffffff ) ) . sprintf( '%04x', $nextTrxId );
|
||||
$this->trxStatus = self::STATUS_TRX_OK;
|
||||
$this->trxStatusCause = null;
|
||||
$this->trxStatusIgnoredCause = null;
|
||||
$this->trxWriteDuration = 0.0;
|
||||
$this->trxWriteQueryCount = 0;
|
||||
|
|
@ -190,14 +174,11 @@ class TransactionManager {
|
|||
|
||||
public function setTrxStatusToOk() {
|
||||
$this->trxStatus = self::STATUS_TRX_OK;
|
||||
$this->trxStatusCause = null;
|
||||
$this->trxStatusIgnoredCause = null;
|
||||
}
|
||||
|
||||
public function setTrxStatusToNone() {
|
||||
$this->trxStatus = self::STATUS_TRX_NONE;
|
||||
$this->trxStatusCause = null;
|
||||
$this->trxStatusIgnoredCause = null;
|
||||
}
|
||||
|
||||
public function assertTransactionStatus( IDatabase $db, $deprecationLogger, $fname ) {
|
||||
|
|
@ -218,14 +199,14 @@ class TransactionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public function assertSessionStatus( IDatabase $db, $fname ) {
|
||||
if ( $this->sessionError ) {
|
||||
throw new DBSessionStateError(
|
||||
public function setTransactionErrorFromStatus( $db, $fname ) {
|
||||
if ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
|
||||
// Put the transaction into an error state if it's not already in one
|
||||
$trxError = new DBUnexpectedError(
|
||||
$db,
|
||||
"Cannot execute query from $fname while session status is ERROR",
|
||||
[],
|
||||
$this->sessionError
|
||||
"Uncancelable atomic section canceled (got $fname)"
|
||||
);
|
||||
$this->setTransactionError( $trxError );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,32 +229,6 @@ class TransactionManager {
|
|||
$this->trxStatusIgnoredCause = $trxStatusIgnoredCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of the current session (ephemeral server-side state tied to the connection)
|
||||
*
|
||||
* @return int One of the STATUS_SESSION_* class constants
|
||||
*/
|
||||
public function sessionStatus() {
|
||||
// Check if an unresolved error still exists
|
||||
return ( $this->sessionError ) ? self::STATUS_SESS_ERROR : self::STATUS_SESS_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag the session as needing a reset due to an error, if not already flagged
|
||||
*
|
||||
* @param Throwable $sessionError
|
||||
*/
|
||||
public function setSessionError( Throwable $sessionError ) {
|
||||
$this->sessionError = $this->sessionError ?? $sessionError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unflag the session as needing a reset due to an error
|
||||
*/
|
||||
public function clearSessionError() {
|
||||
$this->sessionError = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $rtt
|
||||
* @return float Time to apply writes to replicas based on trxWrite* fields
|
||||
|
|
@ -871,20 +826,6 @@ class TransactionManager {
|
|||
return $fnames;
|
||||
}
|
||||
|
||||
/**
|
||||
* List the methods that have precommit callbacks for the current transaction
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function pendingPreCommitCallbackCallers(): array {
|
||||
$fnames = $this->pendingWriteCallers();
|
||||
foreach ( $this->trxPreCommitOrIdleCallbacks as $callback ) {
|
||||
$fnames[] = $callback[1];
|
||||
}
|
||||
|
||||
return $fnames;
|
||||
}
|
||||
|
||||
public function isEndCallbacksSuppressed(): bool {
|
||||
return $this->trxEndCallbacksSuppressed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
* http://www.gnu.org/copyleft/gpl.html
|
||||
*
|
||||
* @file
|
||||
* @ingroup Database
|
||||
*/
|
||||
|
||||
namespace Wikimedia\Rdbms;
|
||||
|
||||
/**
|
||||
* @newable
|
||||
* @ingroup Database
|
||||
*/
|
||||
class DBSessionStateError extends DBTransactionError {
|
||||
}
|
||||
|
|
@ -282,19 +282,6 @@ interface ILBFactory {
|
|||
*/
|
||||
public function rollbackPrimaryChanges( $fname = __METHOD__ );
|
||||
|
||||
/**
|
||||
* Release important session-level state (named lock, table locks) as post-rollback cleanup
|
||||
*
|
||||
* This only applies to the instantiated tracked load balancer instances.
|
||||
*
|
||||
* This should only be called by application entry point functions, since there must be
|
||||
* no chance that a future caller will still be expecting some of the lost session state.
|
||||
*
|
||||
* @param string $fname Caller name
|
||||
* @since 1.37
|
||||
*/
|
||||
public function flushPrimarySessions( $fname = __METHOD__ );
|
||||
|
||||
/**
|
||||
* Check if an explicit transaction round is active
|
||||
*
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ abstract class LBFactory implements ILBFactory {
|
|||
private const ROUND_ROLLING_BACK = 'within-rollback';
|
||||
private const ROUND_COMMIT_CALLBACKS = 'within-commit-callbacks';
|
||||
private const ROUND_ROLLBACK_CALLBACKS = 'within-rollback-callbacks';
|
||||
private const ROUND_ROLLBACK_SESSIONS = 'within-rollback-session';
|
||||
|
||||
private static $loggerFields =
|
||||
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
|
||||
|
|
@ -337,16 +336,6 @@ abstract class LBFactory implements ILBFactory {
|
|||
$this->trxRoundStage = self::ROUND_CURSORY;
|
||||
}
|
||||
|
||||
final public function flushPrimarySessions( $fname = __METHOD__ ) {
|
||||
/** @noinspection PhpUnusedLocalVariableInspection */
|
||||
$scope = ScopedCallback::newScopedIgnoreUserAbort();
|
||||
|
||||
// Release named locks and table locks on all primary DB connections
|
||||
$this->trxRoundStage = self::ROUND_ROLLBACK_SESSIONS;
|
||||
$this->forEachLBCallMethod( 'flushPrimarySessions', [ $fname, $this->id ] );
|
||||
$this->trxRoundStage = self::ROUND_CURSORY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Exception|null
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -636,7 +636,6 @@ interface ILoadBalancer {
|
|||
|
||||
/**
|
||||
* Issue ROLLBACK only on primary, only if queries were done on connection
|
||||
*
|
||||
* @param string $fname Caller name
|
||||
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
|
||||
* @throws DBExpectedError
|
||||
|
|
@ -644,18 +643,6 @@ interface ILoadBalancer {
|
|||
*/
|
||||
public function rollbackPrimaryChanges( $fname = __METHOD__, $owner = null );
|
||||
|
||||
/**
|
||||
* Release/destroy session-level named locks, table locks, and temp tables
|
||||
*
|
||||
* Only call this function right after calling rollbackPrimaryChanges()
|
||||
*
|
||||
* @param string $fname Caller name
|
||||
* @param int|null $owner ID of the calling instance (e.g. the LBFactory ID)
|
||||
* @throws DBExpectedError
|
||||
* @since 1.38
|
||||
*/
|
||||
public function flushPrimarySessions( $fname = __METHOD__, $owner = null );
|
||||
|
||||
/**
|
||||
* Commit all replica DB transactions so as to flush any REPEATABLE-READ or SSI snapshots
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1910,11 +1910,9 @@ class LoadBalancer implements ILoadBalancer {
|
|||
$restore = ( $this->trxRoundId !== false );
|
||||
$this->trxRoundId = false;
|
||||
$this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
|
||||
$this->forEachOpenPrimaryConnection(
|
||||
static function ( IDatabase $conn ) use ( $fname ) {
|
||||
$conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
|
||||
}
|
||||
);
|
||||
$this->forEachOpenPrimaryConnection( static function ( IDatabase $conn ) use ( $fname ) {
|
||||
$conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
|
||||
} );
|
||||
if ( $restore ) {
|
||||
// Unmark handles as participating in this explicit transaction round
|
||||
$this->forEachOpenPrimaryConnection( function ( Database $conn ) {
|
||||
|
|
@ -1924,20 +1922,6 @@ class LoadBalancer implements ILoadBalancer {
|
|||
$this->trxRoundStage = self::ROUND_ROLLBACK_CALLBACKS;
|
||||
}
|
||||
|
||||
public function flushPrimarySessions( $fname = __METHOD__, $owner = null ) {
|
||||
$this->assertTransactionRoundStage( [ self::ROUND_CURSORY ] );
|
||||
if ( $this->hasPrimaryChanges() ) {
|
||||
// Any transaction should have been rolled back beforehand
|
||||
throw new DBTransactionError( null, "Cannot reset session while writes are pending" );
|
||||
}
|
||||
|
||||
$this->forEachOpenPrimaryConnection(
|
||||
static function ( IDatabase $conn ) use ( $fname ) {
|
||||
$conn->flushSession( $fname );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $stage
|
||||
* @throws DBTransactionError
|
||||
|
|
|
|||
|
|
@ -195,10 +195,8 @@ class DatabaseTestHelper extends Database {
|
|||
return $this->lastError ? $this->lastError['error'] : 'test';
|
||||
}
|
||||
|
||||
protected function isKnownStatementRollbackError( $errno ) {
|
||||
return ( $this->lastError['errno'] ?? 0 ) === $errno
|
||||
? ( $this->lastError['wasKnownStatementRollbackError'] ?? false )
|
||||
: false;
|
||||
protected function wasKnownStatementRollbackError() {
|
||||
return $this->lastError['wasKnownStatementRollbackError'] ?? false;
|
||||
}
|
||||
|
||||
public function fieldInfo( $table, $field ) {
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
<?php
|
||||
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Wikimedia\Rdbms\Database;
|
||||
use Wikimedia\Rdbms\DBQueryDisconnectedError;
|
||||
use Wikimedia\Rdbms\DBQueryTimeoutError;
|
||||
use Wikimedia\Rdbms\DBSessionStateError;
|
||||
use Wikimedia\Rdbms\DBTransactionStateError;
|
||||
use Wikimedia\Rdbms\DBUnexpectedError;
|
||||
use Wikimedia\Rdbms\TransactionManager;
|
||||
|
||||
/**
|
||||
* @group mysql
|
||||
* @group Database
|
||||
* @group medium
|
||||
*/
|
||||
class DatabaseMysqlTest extends \MediaWikiIntegrationTestCase {
|
||||
/** @var DatabaseMysqlBase */
|
||||
protected $conn;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
if ( !extension_loaded( 'mysqli' ) ) {
|
||||
$this->markTestSkipped( 'No MySQL support detected' );
|
||||
}
|
||||
|
||||
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
|
||||
if ( $lb->getServerType( 0 ) !== 'mysql' ) {
|
||||
$this->markTestSkipped( 'No MySQL $wgLBFactoryConf config detected' );
|
||||
}
|
||||
|
||||
$this->conn = $this->newConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Database::query()
|
||||
*/
|
||||
public function testQueryTimeout() {
|
||||
try {
|
||||
$this->conn->query(
|
||||
'SET STATEMENT max_statement_time=0.001 FOR SELECT sleep(1) FROM dual',
|
||||
__METHOD__
|
||||
);
|
||||
$this->fail( "No DBQueryTimeoutError caught" );
|
||||
} catch ( DBQueryTimeoutError $e ) {
|
||||
$this->assertInstanceOf( DBQueryTimeoutError::class, $e );
|
||||
}
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Still connected/usable" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Database::query()
|
||||
*/
|
||||
public function testConnectionKill() {
|
||||
try {
|
||||
$this->conn->query( 'KILL (SELECT connection_id())', __METHOD__ );
|
||||
$this->fail( "No DBQueryDisconnectedError caught" );
|
||||
} catch ( DBQueryDisconnectedError $e ) {
|
||||
$this->assertInstanceOf( DBQueryDisconnectedError::class, $e );
|
||||
}
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Recovered" );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Database::query()
|
||||
* @covers Database::rollback()
|
||||
* @covers Database::flushSession()
|
||||
*/
|
||||
public function testConnectionLoss() {
|
||||
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
|
||||
$encId = intval( $row->id );
|
||||
|
||||
$adminConn = $this->newConnection();
|
||||
$adminConn->query( "KILL $encId", __METHOD__ );
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Recovered" );
|
||||
|
||||
$this->conn->startAtomic( __METHOD__ );
|
||||
$this->assertSame( 1, $this->conn->trxLevel(), "Transaction exists" );
|
||||
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
|
||||
$encId = intval( $row->id );
|
||||
|
||||
$adminConn->query( "KILL $encId", __METHOD__ );
|
||||
try {
|
||||
$this->conn->query( 'SELECT 1', __METHOD__ );
|
||||
$this->fail( "No DBQueryDisconnectedError caught" );
|
||||
} catch ( DBQueryDisconnectedError $e ) {
|
||||
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
|
||||
try {
|
||||
$this->conn->endAtomic( __METHOD__ );
|
||||
$this->fail( "No DBUnexpectedError caught" );
|
||||
} catch ( DBUnexpectedError $e ) {
|
||||
$this->assertInstanceOf( DBUnexpectedError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
|
||||
try {
|
||||
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->fail( "No DBTransactionStateError caught" );
|
||||
} catch ( DBTransactionStateError $e ) {
|
||||
$this->assertInstanceOf( DBTransactionStateError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertSame( 0, $this->conn->trxLevel(), "Transaction lost" );
|
||||
$this->conn->rollback( __METHOD__ );
|
||||
$this->assertSame( 0, $this->conn->trxLevel(), "No transaction" );
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Recovered" );
|
||||
}
|
||||
|
||||
$this->conn->lock( 'session_lock', __METHOD__, 0 );
|
||||
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
|
||||
$encId = intval( $row->id );
|
||||
$adminConn->query( "KILL $encId", __METHOD__ );
|
||||
try {
|
||||
$this->conn->query( 'SELECT 1', __METHOD__ );
|
||||
$this->fail( "No DBQueryDisconnectedError caught" );
|
||||
} catch ( DBQueryDisconnectedError $e ) {
|
||||
$this->assertInstanceOf( DBQueryDisconnectedError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
|
||||
try {
|
||||
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->fail( "No DBSessionStateError caught" );
|
||||
} catch ( DBSessionStateError $e ) {
|
||||
$this->assertInstanceOf( DBSessionStateError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertTrue( $this->conn->isOpen(), "Connection remains" );
|
||||
$this->conn->rollback( __METHOD__ );
|
||||
$this->conn->flushSession( __METHOD__ );
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Recovered" );
|
||||
|
||||
$adminConn->close( __METHOD__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Database::query()
|
||||
* @covers Database::cancelAtomic()
|
||||
* @covers Database::rollback()
|
||||
* @covers Database::flushSession()
|
||||
*/
|
||||
public function testTransactionError() {
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_NONE, $this->conn->trxStatus() );
|
||||
|
||||
$this->conn->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_OK, $this->conn->trxStatus() );
|
||||
$row = $this->conn->query( 'SELECT connection_id() AS id', __METHOD__ )->fetchObject();
|
||||
$encId = intval( $row->id );
|
||||
|
||||
try {
|
||||
$this->conn->lock( 'trx_lock', __METHOD__, 0 );
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_OK, $this->conn->trxStatus() );
|
||||
$this->conn->query( "SELECT invalid query()", __METHOD__ );
|
||||
$this->fail( "No DBQueryError caught" );
|
||||
} catch ( DBQueryError $e ) {
|
||||
$this->assertInstanceOf( DBQueryError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
|
||||
try {
|
||||
$this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->fail( "No DBTransactionStateError caught" );
|
||||
} catch ( DBTransactionStateError $e ) {
|
||||
$this->assertInstanceOf( DBTransactionStateError::class, $e );
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
|
||||
$this->assertSame( 1, $this->conn->trxLevel(), "Transaction remains" );
|
||||
$this->assertTrue( $this->conn->isOpen(), "Connection remains" );
|
||||
}
|
||||
|
||||
$adminConn = $this->newConnection();
|
||||
$adminConn->query( "KILL $encId", __METHOD__ );
|
||||
|
||||
$this->assertSame( TransactionManager::STATUS_TRX_ERROR, $this->conn->trxStatus() );
|
||||
try {
|
||||
$this->conn->query( 'SELECT 1', __METHOD__ );
|
||||
$this->fail( "No DBTransactionStateError caught" );
|
||||
} catch ( DBTransactionStateError $e ) {
|
||||
$this->assertInstanceOf( DBTransactionStateError::class, $e );
|
||||
}
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
$this->conn->trxLevel(),
|
||||
"Transaction loss not yet detected (due to STATUS_TRX_ERROR)"
|
||||
);
|
||||
$this->assertTrue(
|
||||
$this->conn->isOpen(),
|
||||
"Connection loss not detected (due to STATUS_TRX_ERROR)"
|
||||
);
|
||||
|
||||
$this->conn->cancelAtomic( __METHOD__ );
|
||||
$this->assertSame( 0, $this->conn->trxLevel(), "No transaction remains" );
|
||||
$this->assertTrue( $this->conn->isOpen(), "Reconnected" );
|
||||
|
||||
$row = $this->conn->query( 'SELECT "x" AS v', __METHOD__ )->fetchObject();
|
||||
$this->assertSame( 'x', $row->v, "Recovered" );
|
||||
|
||||
$this->conn->rollback( __METHOD__ );
|
||||
|
||||
$adminConn->close( __METHOD__ );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DatabaseMysqlBase
|
||||
*/
|
||||
private function newConnection() {
|
||||
$lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
|
||||
/** @var DatabaseMysqlBase $conn */
|
||||
$conn = Database::factory(
|
||||
'mysql',
|
||||
array_merge(
|
||||
$lb->getServerInfo( 0 ),
|
||||
[
|
||||
'dbname' => null,
|
||||
'schema' => null,
|
||||
'tablePrefix' => '',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
$this->conn->close( __METHOD__ );
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue