protected $mConn = null;
protected $mOpened = false;
- /** @var callable[] */
+ /** @var array[] List of (callable, method name) */
protected $mTrxIdleCallbacks = [];
- /** @var callable[] */
+ /** @var array[] List of (callable, method name) */
protected $mTrxPreCommitCallbacks = [];
+ /** @var array[] List of (callable, method name) */
+ protected $mTrxEndCallbacks = [];
protected $mTablePrefix;
protected $mSchema;
return false;
}
- final public function onTransactionIdle( $callback ) {
+ final public function onTransactionResolution( callable $callback ) {
+ if ( !$this->mTrxLevel ) {
+ throw new DBUnexpectedError( $this, "No transaction is active." );
+ }
+ $this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
+ }
+
+ final public function onTransactionIdle( callable $callback ) {
$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
if ( !$this->mTrxLevel ) {
$this->runOnTransactionIdleCallbacks();
}
}
- final public function onTransactionPreCommitOrIdle( $callback ) {
+ final public function onTransactionPreCommitOrIdle( callable $callback ) {
if ( $this->mTrxLevel ) {
$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
} else {
$e = $ePrior = null; // last exception
do { // callbacks may add callbacks :)
- $callbacks = $this->mTrxIdleCallbacks;
+ $callbacks = array_merge(
+ $this->mTrxIdleCallbacks,
+ $this->mTrxEndCallbacks // include "transaction resolution" callbacks
+ );
$this->mTrxIdleCallbacks = []; // recursion guard
+ $this->mTrxEndCallbacks = []; // recursion guard
foreach ( $callbacks as $callback ) {
try {
list( $phpCallback ) = $callback;
$this->getTransactionProfiler()->transactionWritingOut(
$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
}
+
$this->runOnTransactionIdleCallbacks();
}
- # Avoid fatals if close() was called
+ // Avoid fatals if close() was called
$this->assertOpen();
$this->doBegin( $fname );
}
}
- # Avoid fatals if close() was called
+ // Avoid fatals if close() was called
$this->assertOpen();
$this->runOnTransactionPreCommitCallbacks();
$this->getTransactionProfiler()->transactionWritingOut(
$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
}
+
$this->runOnTransactionIdleCallbacks();
}
}
}
- # Avoid fatals if close() was called
+ // Avoid fatals if close() was called
$this->assertOpen();
$this->doRollback( $fname );
- $this->mTrxIdleCallbacks = []; // cancel
- $this->mTrxPreCommitCallbacks = []; // cancel
$this->mTrxAtomicLevels = [];
if ( $this->mTrxDoneWrites ) {
$this->getTransactionProfiler()->transactionWritingOut(
$this->mServer, $this->mDBname, $this->mTrxShortId );
}
+
+ $this->mTrxIdleCallbacks = []; // clear
+ $this->mTrxPreCommitCallbacks = []; // clear
+ $this->runOnTransactionIdleCallbacks();
}
/**
if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
}
- if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
+ $danglingCallbacks = array_merge(
+ $this->mTrxIdleCallbacks,
+ $this->mTrxPreCommitCallbacks,
+ $this->mTrxEndCallbacks
+ );
+ if ( $danglingCallbacks ) {
$callers = [];
- foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
+ foreach ( $danglingCallbacks as $callbackInfo ) {
$callers[] = $callbackInfo[1];
}
$callers = implode( ', ', $callers );
*/
public function getMasterPos();
+ /**
+ * Run an anonymous function as soon as the current transaction commits or rolls back.
+ * An error is thrown if no transaction is pending. Queries in the function will run in
+ * AUTO-COMMIT mode unless there are begin() calls. Callbacks must commit any transactions
+ * that they begin.
+ *
+ * This is useful for combining cooperative locks and DB transactions.
+ *
+ * @param callable $callback
+ * @return mixed
+ * @since 1.28
+ */
+ public function onTransactionResolution( callable $callback );
+
/**
* Run an anonymous function as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
* @param callable $callback
* @since 1.20
*/
- public function onTransactionIdle( $callback );
+ public function onTransactionIdle( callable $callback );
/**
* Run an anonymous function before the current transaction commits or now if there is none.
* @param callable $callback
* @since 1.22
*/
- public function onTransactionPreCommitOrIdle( $callback );
+ public function onTransactionPreCommitOrIdle( callable $callback );
/**
* Begin an atomic section of statements
$db = $this->db;
$db->setFlag( DBO_TRX );
+ $called = false;
$flagSet = null;
- $db->onTransactionIdle( function() use ( $db, &$flagSet ) {
+ $db->onTransactionIdle( function() use ( $db, &$flagSet, &$called ) {
+ $called = true;
$flagSet = $db->getFlag( DBO_TRX );
} );
$this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
$this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
$db->clearFlag( DBO_TRX );
$flagSet = null;
} );
$this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
}
+
+ public function testTransactionResolution() {
+ $db = $this->db;
+
+ $db->clearFlag( DBO_TRX );
+ $db->begin( __METHOD__ );
+ $called = false;
+ $db->onTransactionResolution( function() use ( $db, &$called ) {
+ $called = true;
+ $db->setFlag( DBO_TRX );
+ } );
+ $db->commit( __METHOD__ );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
+
+ $db->clearFlag( DBO_TRX );
+ $db->begin( __METHOD__ );
+ $called = false;
+ $db->onTransactionResolution( function() use ( $db, &$called ) {
+ $called = true;
+ $db->setFlag( DBO_TRX );
+ } );
+ $db->rollback( __METHOD__ );
+ $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
+ $this->assertTrue( $called, 'Callback reached' );
+ }
}