diff --git a/Spanner/src/Database.php b/Spanner/src/Database.php index 14d16d777c3..da631bd32bf 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -964,6 +964,17 @@ public function runTransaction(callable $operation, array $options = []): mixed $this->isRunningTransaction = true; try { $res = call_user_func($operation, $transaction); + } catch (\Throwable $e) { + $active = $transaction->state() === Transaction::STATE_ACTIVE; + $singleUse = $transaction->type() === Transaction::TYPE_SINGLE_USE; + if ($active && !$singleUse) { + try { + $transaction->rollback(); + } catch (\Throwable $rollbackException) { + // ignore rollback failure and bubble up the original exception + } + } + throw $e; } finally { $this->isRunningTransaction = false; } @@ -971,7 +982,7 @@ public function runTransaction(callable $operation, array $options = []): mixed $active = $transaction->state() === Transaction::STATE_ACTIVE; $singleUse = $transaction->type() === Transaction::TYPE_SINGLE_USE; if ($active && !$singleUse) { - $transaction->rollback($options); + $transaction->rollback(); throw new \RuntimeException('Transactions must be rolled back or committed.'); } diff --git a/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index 730ab9ed5d6..e3b0013b6d1 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -792,6 +792,20 @@ public function testRunTransactionNoCommit() $this->database->runTransaction($this->noop()); } + public function testRunTransactionNoCommitWithTag() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Transactions must be rolled back or committed.'); + + $sql = $this->createStreamingAPIArgs()['sql']; + $this->stubExecuteStreamingSql(); + $this->spannerClient->rollback(Argument::cetera())->shouldBeCalled(); + + $this->database->runTransaction(function (Transaction $t) use ($sql) { + $t->execute($sql); + }, ['tag' => self::TRANSACTION_TAG]); + } + public function testRunTransactionNestedTransaction() { $this->expectException(BadMethodCallException::class); @@ -2288,6 +2302,28 @@ public function testRunTransactionWithRollback() }, ['tag' => self::TRANSACTION_TAG]); } + public function testRunTransactionRollsBackOnException() + { + $sql = $this->createStreamingAPIArgs()['sql']; + + $this->stubExecuteStreamingSql(); + $this->spannerClient->rollback( + Argument::that(function ($request) use ($sql) { + return $request->getTransactionId() == self::TRANSACTION; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Callback exception'); + + $this->database->runTransaction(function (Transaction $t) use ($sql) { + $t->execute($sql); + throw new \RuntimeException('Callback exception'); + }, ['tag' => self::TRANSACTION_TAG]); + } + public function testRunTransactionWithExcludeTxnFromChangeStreams() { $sql = 'SELECT example FROM sql_query';