Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@ public function startTransaction(): bool
{
try {
if ($this->inTransaction === 0) {
if ($this->getPDO()->inTransaction()) {
$this->getPDO()->rollBack();
} else {
// If no active transaction, this has no effect.
$this->getPDO()->prepare('ROLLBACK')->execute();
try {
if ($this->getPDO()->inTransaction()) {
$this->getPDO()->rollBack();
} else {
// If no active transaction, this has no effect.
$this->getPDO()->prepare('ROLLBACK')->execute();
}
} catch (PDOException) {
// A pooled connection can report a transaction it no longer
// holds after a reconnect (e.g. Swoole PDOProxy keeps its own
// counter), making this cleanup rollback throw. It is best
// effort; swallow it and begin a fresh transaction below.
}

$result = $this->getPDO()->beginTransaction();
Expand Down
17 changes: 12 additions & 5 deletions src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,18 @@ public function startTransaction(): bool
{
try {
if ($this->inTransaction === 0) {
if ($this->getPDO()->inTransaction()) {
$this->getPDO()->rollBack();
} else {
// If no active transaction, this has no effect.
$this->getPDO()->prepare('ROLLBACK')->execute();
try {
if ($this->getPDO()->inTransaction()) {
$this->getPDO()->rollBack();
} else {
// If no active transaction, this has no effect.
$this->getPDO()->prepare('ROLLBACK')->execute();
}
} catch (PDOException) {
// A pooled connection can report a transaction it no longer
// holds after a reconnect (e.g. Swoole PDOProxy keeps its own
// counter), making this cleanup rollback throw. It is best
// effort; swallow it and begin a fresh transaction below.
}
Comment thread
abnegate marked this conversation as resolved.

$this->getPDO()->beginTransaction();
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/SQLTransactionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Tests\Unit;

use PDOException;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Adapter\MySQL;
use Utopia\Database\Adapter\Postgres;
use Utopia\Database\Exception\Transaction as TransactionException;

class SQLTransactionTest extends TestCase
{
/**
* A pooled connection (e.g. Swoole PDOProxy) can keep its own transaction
* counter that survives a reconnect, so it reports an open transaction the
* underlying connection no longer holds. The cleanup rollBack() then throws
* "There is no active transaction". startTransaction() must swallow that and
* still begin a fresh transaction instead of surfacing it as a failure.
*/
public function testStartTransactionRecoversFromDesyncedRollback(): void
{
$pdo = $this->getMockBuilder(\PDO::class)
->disableOriginalConstructor()
->getMock();

$pdo->method('inTransaction')->willReturn(true);
$pdo->method('rollBack')->willThrowException(
new PDOException('There is no active transaction')
);
$pdo->expects($this->once())
->method('beginTransaction')
->willReturn(true);

$adapter = new MySQL($pdo);

$this->assertTrue($adapter->startTransaction());
$this->assertTrue($adapter->inTransaction());
}

public function testStartTransactionDoesNotMaskBeginFailureAfterDesyncedRollback(): void
{
$pdo = $this->getMockBuilder(\PDO::class)
->disableOriginalConstructor()
->getMock();

$pdo->method('inTransaction')->willReturn(true);
$pdo->method('rollBack')->willThrowException(
new PDOException('There is no active transaction')
);
$pdo->expects($this->once())
->method('beginTransaction')
->willThrowException(new PDOException('Connection lost'));

$adapter = new MySQL($pdo);

$this->expectException(TransactionException::class);
$this->expectExceptionMessage('Failed to start transaction: Connection lost');

$adapter->startTransaction();
}

public function testPostgresStartTransactionRecoversFromDesyncedRollback(): void
{
$pdo = $this->getMockBuilder(\PDO::class)
->disableOriginalConstructor()
->getMock();

$pdo->method('inTransaction')->willReturn(true);
$pdo->method('rollBack')->willThrowException(
new PDOException('There is no active transaction')
);
$pdo->expects($this->once())
->method('beginTransaction')
->willReturn(true);

$adapter = new Postgres($pdo);

$this->assertTrue($adapter->startTransaction());
$this->assertTrue($adapter->inTransaction());
}
}
Loading