if ( $this->conn ) {
// Resolve any dangling transaction first
if ( $this->trxLevel ) {
- // Meaningful transactions should ideally have been resolved by now
- if ( $this->writesOrCallbacksPending() ) {
- $this->queryLogger->warning(
- __METHOD__ . ": writes or callbacks still pending.",
- [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
- );
+ if ( $this->trxAtomicLevels ) {
// Cannot let incomplete atomic sections be committed
- if ( $this->trxAtomicLevels ) {
- $levels = $this->flatAtomicSectionList();
- $exception = new DBUnexpectedError(
- $this,
- __METHOD__ . ": atomic sections $levels are still open."
- );
- // Check if it is possible to properly commit and trigger callbacks
- } elseif ( $this->trxEndCallbacksSuppressed ) {
+ $levels = $this->flatAtomicSectionList();
+ $exception = new DBUnexpectedError(
+ $this,
+ __METHOD__ . ": atomic sections $levels are still open."
+ );
+ } elseif ( $this->trxAutomatic ) {
+ // Only the connection manager can commit non-empty DBO_TRX transactions
+ if ( $this->writesOrCallbacksPending() ) {
$exception = new DBUnexpectedError(
$this,
- __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ __METHOD__ .
+ ": mass commit/rollback of peer transaction required (DBO_TRX set)."
);
}
+ } elseif ( $this->trxLevel ) {
+ // Commit explicit transactions as if this was commit()
+ $this->queryLogger->warning(
+ __METHOD__ . ": writes or callbacks still pending.",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+ );
+ }
+
+ if ( $this->trxEndCallbacksSuppressed ) {
+ $exception = $exception ?: new DBUnexpectedError(
+ $this,
+ __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ );
}
+
// Commit or rollback the changes and run any callbacks as needed
if ( $this->trxStatus === self::STATUS_TRX_OK && !$exception ) {
- $this->commit( __METHOD__, self::TRANSACTION_INTERNAL );
+ $this->commit(
+ __METHOD__,
+ $this->trxAutomatic ? self::FLUSHING_INTERNAL : self::FLUSHING_ONE
+ );
} else {
- $this->rollback( __METHOD__, self::TRANSACTION_INTERNAL );
+ $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
}
}
+
// Close the actual connection in the binding handle
$closed = $this->closeConnection();
$this->conn = false;
}
final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+ static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
+ if ( !in_array( $mode, $modes, true ) ) {
+ throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
+ }
+
// Protect against mismatched atomic section, transaction nesting, and snapshot loss
if ( $this->trxLevel ) {
if ( $this->trxAtomicLevels ) {
$this->trxLevel = 1;
}
- final public function commit( $fname = __METHOD__, $flush = '' ) {
+ final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
+ static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
+ if ( !in_array( $flush, $modes, true ) ) {
+ throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
+ }
+
if ( $this->trxLevel && $this->trxAtomicLevels ) {
- // There are still atomic sections open. This cannot be ignored
+ // There are still atomic sections open; this cannot be ignored
$levels = $this->flatAtomicSectionList();
throw new DBUnexpectedError(
$this,
$this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
$this->assertEquals( 0, $this->database->trxLevel() );
}
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose3() {
+ try {
+ $this->database->setFlag( IDatabase::DBO_TRX );
+ $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+ $this->assertEquals( 1, $this->database->trxLevel() );
+ $this->database->close();
+ $this->fail( 'Expected exception not thrown' );
+ } catch ( DBUnexpectedError $ex ) {
+ $this->assertSame(
+ 'Wikimedia\Rdbms\Database::close: ' .
+ 'mass commit/rollback of peer transaction required (DBO_TRX set).',
+ $ex->getMessage()
+ );
+ }
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
+
+ /**
+ * @covers \Wikimedia\Rdbms\Database::close
+ */
+ public function testPrematureClose4() {
+ $this->database->setFlag( IDatabase::DBO_TRX );
+ $this->database->query( 'SELECT 1', __METHOD__ );
+ $this->assertEquals( 1, $this->database->trxLevel() );
+ $this->database->close();
+ $this->database->clearFlag( IDatabase::DBO_TRX );
+
+ $this->assertFalse( $this->database->isOpen() );
+ $this->assertLastSql( 'BEGIN; SELECT 1; COMMIT' );
+ $this->assertEquals( 0, $this->database->trxLevel() );
+ }
}