use WANObjectCache;
use Exception;
use RuntimeException;
+use LogicException;
/**
* An interface for generating database load balancers
/** @var string Agent name for query profiling */
protected $agent;
+ /** @var string One of the ROUND_* class constants */
+ private $trxRoundStage = self::ROUND_CURSORY;
+
+ const ROUND_CURSORY = 'cursory';
+ const ROUND_BEGINNING = 'within-begin';
+ const ROUND_COMMITTING = 'within-commit';
+ const ROUND_ROLLING_BACK = 'within-rollback';
+
private static $loggerFields =
[ 'replLogger', 'connLogger', 'queryLogger', 'perfLogger' ];
$this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
}
- public function commitAll( $fname = __METHOD__, array $options = [] ) {
+ final public function commitAll( $fname = __METHOD__, array $options = [] ) {
$this->commitMasterChanges( $fname, $options );
- $this->forEachLBCallMethod( 'commitAll', [ $fname ] );
+ $this->forEachLBCallMethod( 'flushMasterSnapshots', [ $fname ] );
+ $this->forEachLBCallMethod( 'flushReplicaSnapshots', [ $fname ] );
}
- public function beginMasterChanges( $fname = __METHOD__ ) {
+ final public function beginMasterChanges( $fname = __METHOD__ ) {
+ $this->assertTransactionRoundStage( self::ROUND_CURSORY );
+ $this->trxRoundStage = self::ROUND_BEGINNING;
if ( $this->trxRoundId !== false ) {
throw new DBTransactionError(
null,
$this->trxRoundId = $fname;
// Set DBO_TRX flags on all appropriate DBs
$this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
+ $this->trxRoundStage = self::ROUND_CURSORY;
}
- public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
+ final public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
+ $this->assertTransactionRoundStage( self::ROUND_CURSORY );
+ $this->trxRoundStage = self::ROUND_COMMITTING;
if ( $this->trxRoundId !== false && $this->trxRoundId !== $fname ) {
throw new DBTransactionError(
null,
/** @noinspection PhpUnusedLocalVariableInspection */
$scope = $this->getScopedPHPBehaviorForCommit(); // try to ignore client aborts
// Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
- $this->forEachLBCallMethod( 'finalizeMasterChanges' );
+ do {
+ $count = 0; // number of callbacks executed this iteration
+ $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$count ) {
+ $count += $lb->finalizeMasterChanges();
+ } );
+ } while ( $count > 0 );
$this->trxRoundId = false;
// Perform pre-commit checks, aborting on failure
$this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
$this->logIfMultiDbTransaction();
// Actually perform the commit on all master DB connections and revert DBO_TRX
$this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
- // Run all post-commit callbacks
- /** @var Exception $e */
- $e = null; // first callback exception
- $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
- $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
- $e = $e ?: $ex;
- } );
- // Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
- $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
+ // Run all post-commit callbacks in a separate step
+ $e = $this->executePostTransactionCallbacks();
+ $this->trxRoundStage = self::ROUND_CURSORY;
// Throw any last post-commit callback error
if ( $e instanceof Exception ) {
throw $e;
}
}
- public function rollbackMasterChanges( $fname = __METHOD__ ) {
+ final public function rollbackMasterChanges( $fname = __METHOD__ ) {
+ $this->trxRoundStage = self::ROUND_ROLLING_BACK;
$this->trxRoundId = false;
- $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
+ // Actually perform the rollback on all master DB connections and revert DBO_TRX
$this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
- // Run all post-rollback callbacks
- $this->forEachLB( function ( ILoadBalancer $lb ) {
- $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
+ // Run all post-commit callbacks in a separate step
+ $this->executePostTransactionCallbacks();
+ $this->trxRoundStage = self::ROUND_CURSORY;
+ }
+
+ /**
+ * @return Exception|null
+ */
+ private function executePostTransactionCallbacks() {
+ // Run all post-commit callbacks until new ones stop getting added
+ $e = null; // first callback exception
+ do {
+ $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
+ $ex = $lb->runMasterTransactionIdleCallbacks();
+ $e = $e ?: $ex;
+ } );
+ } while ( $this->hasMasterChanges() );
+ // Run all listener callbacks once
+ $this->forEachLB( function ( ILoadBalancer $lb ) use ( &$e ) {
+ $ex = $lb->runMasterTransactionListenerCallbacks();
+ $e = $e ?: $ex;
} );
+
+ return $e;
}
public function hasTransactionRound() {
return ( $this->trxRoundId !== false );
}
+ public function isReadyForRoundOperations() {
+ return ( $this->trxRoundStage === self::ROUND_CURSORY );
+ }
+
/**
* Log query info if multi DB transactions are going to be committed now
*/
return $this->ticket;
}
- public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
+ final public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
if ( $ticket !== $this->ticket ) {
$this->perfLogger->error( __METHOD__ . ": $fname does not have outer scope.\n" .
( new RuntimeException() )->getTraceAsString() );
'hostname' => $this->hostname,
'cliMode' => $this->cliMode,
'agent' => $this->agent,
- 'chronologyProtector' => $this->getChronologyProtector()
+ 'chronologyCallback' => function ( ILoadBalancer $lb ) {
+ // Defer ChronologyProtector construction in case setRequestInfo() ends up
+ // being called later (but before the first connection attempt) (T192611)
+ $this->getChronologyProtector()->initLB( $lb );
+ }
];
}
}
public function setRequestInfo( array $info ) {
+ if ( $this->chronProt ) {
+ throw new LogicException( 'ChronologyProtector already initialized.' );
+ }
+
$this->requestInfo = $info + $this->requestInfo;
}
+ /**
+ * @param string $stage
+ */
+ private function assertTransactionRoundStage( $stage ) {
+ if ( $this->trxRoundStage !== $stage ) {
+ throw new DBTransactionError(
+ null,
+ "Transaction round stage must be '$stage' (not '{$this->trxRoundStage}')"
+ );
+ }
+ }
+
/**
* Make PHP ignore user aborts/disconnects until the returned
* value leaves scope. This returns null and does nothing in CLI mode.