return $this->__call( __FUNCTION__, func_get_args() );
}
+ public function preCommitCallbacksPending() {
+ return $this->__call( __FUNCTION__, func_get_args() );
+ }
+
public function writesOrCallbacksPending() {
return $this->__call( __FUNCTION__, func_get_args() );
}
);
}
+ public function preCommitCallbacksPending() {
+ return $this->trxLevel && $this->trxPreCommitCallbacks;
+ }
+
/**
* @return string|null
*/
public function writesPending();
/**
- * Returns true if there is a transaction/round open with possible write
- * queries or transaction pre-commit/idle callbacks waiting on it to finish.
+ * @return bool Whether there is a transaction open with pre-commit callbacks pending
+ * @since 1.32
+ */
+ public function preCommitCallbacksPending();
+
+ /**
+ * Whether there is a transaction open with either possible write queries
+ * or unresolved pre-commit/commit/resolution callbacks pending
+ *
* This does *not* count recurring callbacks, e.g. from setTransactionListener().
*
* @return bool
/** @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 ] );
* Run pre-commit callbacks and defer execution of post-commit callbacks
*
* Use this only for mutli-database commits
+ *
+ * @return int Number of pre-commit callbacks run (since 1.32)
*/
public function finalizeMasterChanges();
public function hasMasterConnection();
/**
- * Determine if there are pending changes in a transaction by this thread
+ * Whether there are pending changes or callbacks in a transaction by this thread
* @return bool
*/
public function hasMasterChanges();
}
public function finalizeMasterChanges() {
- $this->assertTransactionRoundStage( self::ROUND_CURSORY );
+ $this->assertTransactionRoundStage( [ self::ROUND_CURSORY, self::ROUND_FINALIZED ] );
$this->trxRoundStage = self::ROUND_ERROR; // "failed" until proven otherwise
// Loop until callbacks stop adding callbacks on other connections
+ $total = 0;
do {
$count = 0; // callbacks execution attempts
$this->forEachOpenMasterConnection( function ( Database $conn ) use ( &$count ) {
// Any error should cause all (peer) transactions to be rolled back together.
$count += $conn->runOnTransactionPreCommitCallbacks();
} );
+ $total += $count;
} while ( $count > 0 );
// Defer post-commit callbacks until after COMMIT/ROLLBACK happens on all handles
$this->forEachOpenMasterConnection( function ( Database $conn ) {
$conn->setTrxEndCallbackSuppression( true );
} );
$this->trxRoundStage = self::ROUND_FINALIZED;
+
+ return $total;
}
public function approveMasterChanges( array $options ) {
}
/**
- * @param string $stage
+ * @param string|string[] $stage
*/
private function assertTransactionRoundStage( $stage ) {
- if ( $this->trxRoundStage !== $stage ) {
+ $stages = (array)$stage;
+
+ if ( !in_array( $this->trxRoundStage, $stages, true ) ) {
+ $stageList = implode(
+ '/',
+ array_map( function ( $v ) {
+ return "'$v'";
+ }, $stages )
+ );
throw new DBTransactionError(
null,
- "Transaction round stage must be '$stage' (not '{$this->trxRoundStage}')"
+ "Transaction round stage must be $stageList (not '{$this->trxRoundStage}')"
);
}
}
global $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword, $wgDBtype, $wgSQLiteDataDir;
$factory = new LBFactoryMulti( [
- 'sectionsByDB' => [],
+ 'sectionsByDB' => [
+ 's1wiki' => 's1',
+ ],
'sectionLoads' => [
+ 's1' => [
+ 'test-db3' => 0,
+ 'test-db4' => 100,
+ ],
'DEFAULT' => [
'test-db1' => 0,
'test-db2' => 100,
- ],
+ ]
],
'serverTemplate' => [
'dbname' => $wgDBname,
],
'hostsByName' => [
'test-db1' => $wgDBserver,
- 'test-db2' => $wgDBserver
+ 'test-db2' => $wgDBserver,
+ 'test-db3' => $wgDBserver,
+ 'test-db4' => $wgDBserver
],
'loadMonitorClass' => LoadMonitorNull::class
] );
$dbr = $lb->getConnection( DB_REPLICA );
$this->assertTrue( $dbr->getLBInfo( 'replica' ), 'slave shows as slave' );
+ // Test that LoadBalancer instances made during commitMasterChanges() do not throw
+ // DBTransactionError due to transaction ROUND_* stages being mismatched.
+ $factory->beginMasterChanges( __METHOD__ );
+ $dbw->onTransactionPreCommitOrIdle( function () use ( $factory ) {
+ // Trigger s1 LoadBalancer instantiation during "finalize" stage.
+ // There is no s1wiki DB to select so it is not in getConnection(),
+ // but this fools getMainLB() at least.
+ $factory->getMainLB( 's1wiki' )->getConnection( DB_MASTER );
+ } );
+ $factory->commitMasterChanges( __METHOD__ );
+
+ $count = 0;
+ $factory->forEachLB( function () use ( &$count ) {
+ ++$count;
+ } );
+ $this->assertEquals( 2, $count );
+
$factory->shutdown();
- $lb->closeAll();
+ $factory->closeAll();
}
/**