/** @var bool */
protected $opened = false;
- /** @var array[] List of (callable, method name) */
+ /** @var array[] List of (callable, method name, atomic section id) */
protected $trxIdleCallbacks = [];
- /** @var array[] List of (callable, method name) */
+ /** @var array[] List of (callable, method name, atomic section id) */
protected $trxPreCommitCallbacks = [];
- /** @var array[] List of (callable, method name) */
+ /** @var array[] List of (callable, method name, atomic section id) */
protected $trxEndCallbacks = [];
/** @var callable[] Map of (name => callable) */
protected $trxRecurringCallbacks = [];
/** @var int */
protected $nonNativeInsertSelectBatchSize = 10000;
+ /** @var string Idiom used when a cancelable atomic section started the transaction */
+ private static $NOT_APPLICABLE = 'n/a';
+ /** @var string Prefix to the atomic section counter used to make savepoint IDs */
+ private static $SAVEPOINT_PREFIX = 'wikimedia_rdbms_atomic';
+
/** @var int Transaction is in a error state requiring a full or savepoint rollback */
const STATUS_TRX_ERROR = 1;
/** @var int Transaction is active and in a normal state */
if ( !$this->trxLevel ) {
throw new DBUnexpectedError( $this, "No transaction is active." );
}
- $this->trxEndCallbacks[] = [ $callback, $fname ];
+ $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
}
final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
$this->trxAutomatic = true;
}
- $this->trxIdleCallbacks[] = [ $callback, $fname ];
+ $this->trxIdleCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
if ( !$this->trxLevel ) {
$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
}
}
if ( $this->trxLevel ) {
- $this->trxPreCommitCallbacks[] = [ $callback, $fname ];
+ $this->trxPreCommitCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
} else {
// No transaction is active nor will start implicitly, so make one for this callback
$this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
}
}
+ /**
+ * @return AtomicSectionIdentifier|null ID of the topmost atomic section level
+ */
+ private function currentAtomicSectionId() {
+ if ( $this->trxLevel && $this->trxAtomicLevels ) {
+ $levelInfo = end( $this->trxAtomicLevels );
+
+ return $levelInfo[1];
+ }
+
+ return null;
+ }
+
+ /**
+ * @param AtomicSectionIdentifier $old
+ * @param AtomicSectionIdentifier $new
+ */
+ private function reassignCallbacksForSection(
+ AtomicSectionIdentifier $old, AtomicSectionIdentifier $new
+ ) {
+ foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxPreCommitCallbacks[$key][2] = $new;
+ }
+ }
+ foreach ( $this->trxIdleCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxIdleCallbacks[$key][2] = $new;
+ }
+ }
+ foreach ( $this->trxEndCallbacks as $key => $info ) {
+ if ( $info[2] === $old ) {
+ $this->trxEndCallbacks[$key][2] = $new;
+ }
+ }
+ }
+
+ /**
+ * @param AtomicSectionIdentifier[] $sectionIds ID of an actual savepoint
+ * @throws UnexpectedValueException
+ */
+ private function modifyCallbacksForCancel( array $sectionIds ) {
+ // Cancel the "on commit" callbacks owned by this savepoint
+ $this->trxIdleCallbacks = array_filter(
+ $this->trxIdleCallbacks,
+ function ( $entry ) use ( $sectionIds ) {
+ return !in_array( $entry[2], $sectionIds, true );
+ }
+ );
+ $this->trxPreCommitCallbacks = array_filter(
+ $this->trxPreCommitCallbacks,
+ function ( $entry ) use ( $sectionIds ) {
+ return !in_array( $entry[2], $sectionIds, true );
+ }
+ );
+ // Make "on resolution" callbacks owned by this savepoint to perceive a rollback
+ foreach ( $this->trxEndCallbacks as $key => $entry ) {
+ if ( in_array( $entry[2], $sectionIds, true ) ) {
+ $callback = $entry[0];
+ $this->trxEndCallbacks[$key][0] = function () use ( $callback ) {
+ return $callback( self::TRIGGER_ROLLBACK );
+ };
+ }
+ }
+ }
+
final public function setTransactionListener( $name, callable $callback = null ) {
if ( $callback ) {
$this->trxRecurringCallbacks[$name] = $callback;
$this->query( 'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier ), $fname );
}
+ /**
+ * @param string $fname
+ * @return string
+ */
+ private function nextSavepointId( $fname ) {
+ $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
+ if ( strlen( $savepointId ) > 30 ) {
+ // 30 == Oracle's identifier length limit (pre 12c)
+ // With a 22 character prefix, that puts the highest number at 99999999.
+ throw new DBUnexpectedError(
+ $this,
+ 'There have been an excessively large number of atomic sections in a transaction'
+ . " started by $this->trxFname (at $fname)"
+ );
+ }
+
+ return $savepointId;
+ }
+
final public function startAtomic(
$fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
) {
- $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? 'n/a' : null;
+ $savepointId = $cancelable === self::ATOMIC_CANCELABLE ? self::$NOT_APPLICABLE : null;
+
if ( !$this->trxLevel ) {
$this->begin( $fname, self::TRANSACTION_INTERNAL );
// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
// in all changes being in one transaction to keep requests transactional.
- if ( !$this->getFlag( self::DBO_TRX ) ) {
+ if ( $this->getFlag( self::DBO_TRX ) ) {
+ // Since writes could happen in between the topmost atomic sections as part
+ // of the transaction, those sections will need savepoints.
+ $savepointId = $this->nextSavepointId( $fname );
+ $this->doSavepoint( $savepointId, $fname );
+ } else {
$this->trxAutomaticAtomic = true;
}
} elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
- $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
- if ( strlen( $savepointId ) > 30 ) { // 30 == Oracle's identifier length limit (pre 12c)
- $this->queryLogger->warning(
- 'There have been an excessively large number of atomic sections in a transaction'
- . " started by $this->trxFname, reusing IDs (at $fname)",
- [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
- );
- $this->trxAtomicCounter = 0;
- $savepointId = 'wikimedia_rdbms_atomic' . ++$this->trxAtomicCounter;
- }
+ $savepointId = $this->nextSavepointId( $fname );
$this->doSavepoint( $savepointId, $fname );
}
// Check if the current section matches $fname
$pos = count( $this->trxAtomicLevels ) - 1;
- list( $savedFname, , $savepointId ) = $this->trxAtomicLevels[$pos];
+ list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
if ( $savedFname !== $fname ) {
throw new DBUnexpectedError(
);
}
- // Remove the last section and re-index the array
- $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos );
+ // Remove the last section (no need to re-index the array)
+ array_pop( $this->trxAtomicLevels );
if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
$this->commit( $fname, self::FLUSHING_INTERNAL );
- } elseif ( $savepointId !== null && $savepointId !== 'n/a' ) {
+ } elseif ( $savepointId !== null && $savepointId !== self::$NOT_APPLICABLE ) {
$this->doReleaseSavepoint( $savepointId, $fname );
}
+
+ // Hoist callback ownership for callbacks in the section that just ended;
+ // all callbacks should have an owner that is present in trxAtomicLevels.
+ $currentSectionId = $this->currentAtomicSectionId();
+ if ( $currentSectionId ) {
+ $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
+ }
}
final public function cancelAtomic(
throw new DBUnexpectedError( "Atomic section not found (for $fname)" );
}
// Remove all descendant sections and re-index the array
+ $excisedIds = [];
+ $len = count( $this->trxAtomicLevels );
+ for ( $i = $pos + 1; $i < $len; ++$i ) {
+ $excisedIds[] = $this->trxAtomicLevels[$i][1];
+ }
$this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
+ $this->modifyCallbacksForCancel( $excisedIds );
}
// Check if the current section matches $fname
$pos = count( $this->trxAtomicLevels ) - 1;
- list( $savedFname, , $savepointId ) = $this->trxAtomicLevels[$pos];
+ list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
if ( $savedFname !== $fname ) {
throw new DBUnexpectedError(
);
}
- // Remove the last section and re-index the array
- $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos );
+ // Remove the last section (no need to re-index the array)
+ array_pop( $this->trxAtomicLevels );
+ $this->modifyCallbacksForCancel( [ $savedSectionId ] );
if ( $savepointId !== null ) {
// Rollback the transaction to the state just before this atomic section
- if ( $savepointId === 'n/a' ) {
+ if ( $savepointId === self::$NOT_APPLICABLE ) {
$this->rollback( $fname, self::FLUSHING_INTERNAL );
} else {
$this->doRollbackToSavepoint( $savepointId, $fname );
// phpcs:ignore Generic.Files.LineLength
$this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
+ $fname = __METHOD__;
+ $triggerMap = [
+ '-' => '-',
+ IDatabase::TRIGGER_COMMIT => 'tCommit',
+ IDatabase::TRIGGER_ROLLBACK => 'tRollback'
+ ];
+ $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+ $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname );
+ };
+ $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+ $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname );
+ };
+ $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) {
+ $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname );
+ };
+
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionResolution( $callback1, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' );
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback3, __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertLastSql( implode( "; ", [
+ 'BEGIN',
+ 'SAVEPOINT wikimedia_rdbms_atomic1',
+ 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+ 'SELECT 1, - AS t',
+ 'SELECT 3, - AS t',
+ 'COMMIT'
+ ] ) );
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionIdle( $callback2, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->onTransactionIdle( $callback3, __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertLastSql( implode( "; ", [
+ 'BEGIN',
+ 'SAVEPOINT wikimedia_rdbms_atomic1',
+ 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+ 'COMMIT',
+ 'SELECT 1, tCommit AS t',
+ 'SELECT 3, tCommit AS t'
+ ] ) );
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->onTransactionResolution( $callback1, __METHOD__ );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionResolution( $callback2, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertLastSql( implode( "; ", [
+ 'BEGIN',
+ 'SAVEPOINT wikimedia_rdbms_atomic1',
+ 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+ 'COMMIT',
+ 'SELECT 1, tCommit AS t',
+ 'SELECT 2, tRollback AS t',
+ 'SELECT 3, tCommit AS t'
+ ] ) );
+
+ $makeCallback = function ( $id ) use ( $fname, $triggerMap ) {
+ return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) {
+ $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname );
+ };
+ };
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertLastSql( implode( "; ", [
+ 'BEGIN',
+ 'SAVEPOINT wikimedia_rdbms_atomic1',
+ 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+ 'COMMIT',
+ 'SELECT 1, tRollback AS t'
+ ] ) );
+
+ $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ );
+ $this->database->startAtomic( __METHOD__ . '_level2' );
+ $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ . '_level3' );
+ $this->database->endAtomic( __METHOD__ . '_level2' );
+ $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_level1' );
+ $this->assertLastSql( implode( "; ", [
+ 'BEGIN',
+ 'SAVEPOINT wikimedia_rdbms_atomic1',
+ 'SAVEPOINT wikimedia_rdbms_atomic2',
+ 'RELEASE SAVEPOINT wikimedia_rdbms_atomic2',
+ 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1',
+ 'COMMIT; SELECT 1, tCommit AS t',
+ 'SELECT 2, tRollback AS t',
+ 'SELECT 3, tRollback AS t',
+ 'SELECT 4, tCommit AS t'
+ ] ) );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::doSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+ * @covers \Wikimedia\Rdbms\Database::startAtomic
+ * @covers \Wikimedia\Rdbms\Database::endAtomic
+ * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+ * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+ */
+ public function testAtomicSectionsRecovery() {
$this->database->begin( __METHOD__ );
try {
$this->database->doAtomicSection(
} catch ( RuntimeException $ex ) {
$this->assertSame( 'Test exception', $ex->getMessage() );
}
+ try {
+ $this->database->commit( __METHOD__ );
+ $this->fail( 'Test exception not thrown' );
+ } catch ( DBTransactionError $ex ) {
+ $this->assertSame(
+ 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.',
+ $ex->getMessage()
+ );
+ }
$this->database->rollback( __METHOD__ );
$this->assertLastSql( 'BEGIN; ROLLBACK' );
}
+ /**
+ * @covers \Wikimedia\Rdbms\Database::doSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+ * @covers \Wikimedia\Rdbms\Database::startAtomic
+ * @covers \Wikimedia\Rdbms\Database::endAtomic
+ * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+ * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+ */
+ public function testAtomicSectionsCallbackCancellation() {
+ $fname = __METHOD__;
+ $callback1Called = null;
+ $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) {
+ $callback1Called = $trigger;
+ $this->database->query( "SELECT 1", $fname );
+ };
+ $callback2Called = null;
+ $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) {
+ $callback2Called = $trigger;
+ $this->database->query( "SELECT 2", $fname );
+ };
+ $callback3Called = null;
+ $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) {
+ $callback3Called = $trigger;
+ $this->database->query( "SELECT 3", $fname );
+ };
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_inner' );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ // phpcs:ignore Generic.Files.LineLength
+ $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+
+ $callback1Called = null;
+ $callback2Called = null;
+ $callback3Called = null;
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_inner' );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ // phpcs:ignore Generic.Files.LineLength
+ $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' );
+
+ $callback1Called = null;
+ $callback2Called = null;
+ $callback3Called = null;
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__, $atomicId );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+ $callback1Called = null;
+ $callback2Called = null;
+ $callback3Called = null;
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ try {
+ $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId );
+ } catch ( DBUnexpectedError $e ) {
+ $m = __METHOD__;
+ $this->assertSame(
+ "Invalid atomic section ended (got {$m}_X but expected {$m}).",
+ $e->getMessage()
+ );
+ }
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $this->database->cancelAtomic( __METHOD__ . '_inner' );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+
+ $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+ $callback1Called = null;
+ $callback2Called = null;
+ $callback3Called = null;
+ $this->database->startAtomic( __METHOD__ . '_outer' );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->startAtomic( __METHOD__ . '_inner' );
+ $this->database->onTransactionIdle( $callback1, __METHOD__ );
+ $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ );
+ $this->database->onTransactionResolution( $callback3, __METHOD__ );
+ $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+ $this->database->cancelAtomic( __METHOD__ . '_inner' );
+ $this->database->cancelAtomic( __METHOD__ );
+ $this->database->endAtomic( __METHOD__ . '_outer' );
+ $this->assertNull( $callback1Called );
+ $this->assertNull( $callback2Called );
+ $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::doSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint
+ * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint
+ * @covers \Wikimedia\Rdbms\Database::startAtomic
+ * @covers \Wikimedia\Rdbms\Database::endAtomic
+ * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+ * @covers \Wikimedia\Rdbms\Database::doAtomicSection
+ */
+ public function testAtomicSectionsTrxRound() {
+ $this->database->setFlag( IDatabase::DBO_TRX );
+ $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+ $this->database->query( 'SELECT 1', __METHOD__ );
+ $this->database->endAtomic( __METHOD__ );
+ $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
+ // phpcs:ignore Generic.Files.LineLength
+ $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+ }
+
public static function provideAtomicSectionMethodsForErrors() {
return [
[ 'endAtomic' ],