Skip to content

Commit

Permalink
Merge pull request #6812 from lcobucci/convert-exceptions-on-begin-tr…
Browse files Browse the repository at this point in the history
…ansaction

Convert driver exceptions when starting transactions
  • Loading branch information
morozov authored Mar 1, 2025
2 parents 369ab24 + b6eb520 commit 598b2a3
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 13 deletions.
6 changes: 5 additions & 1 deletion src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,11 @@ public function beginTransaction(): void
++$this->transactionNestingLevel;

if ($this->transactionNestingLevel === 1) {
$connection->beginTransaction();
try {
$connection->beginTransaction();
} catch (Driver\Exception $e) {
throw $this->convertException($e);
}
} else {
$this->createSavepoint($this->_getNestedTransactionSavePointName());
}
Expand Down
5 changes: 5 additions & 0 deletions src/Driver/API/PostgreSQL/ExceptionConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
Expand Down Expand Up @@ -77,6 +78,10 @@ public function convert(Exception $exception, ?Query $query): DriverException
return new ConnectionException($exception, $query);
}

if (str_contains($exception->getMessage(), 'terminating connection')) {
return new ConnectionLost($exception, $query);
}

return new DriverException($exception, $query);
}
}
4 changes: 3 additions & 1 deletion src/Driver/Mysqli/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ public function lastInsertId(): int|string

public function beginTransaction(): void
{
$this->connection->begin_transaction();
if (! $this->connection->begin_transaction()) {
throw ConnectionError::new($this->connection);
}
}

public function commit(): void
Expand Down
50 changes: 39 additions & 11 deletions tests/Functional/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,48 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Doctrine\DBAL\Types\Types;

use function func_get_args;
use function restore_error_handler;
use function set_error_handler;
use function sleep;

use const E_WARNING;

class TransactionTest extends FunctionalTestCase
{
public function testBeginTransactionFailure(): void
{
$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->beginTransaction();
});
}

public function testCommitFailure(): void
{
$this->connection->beginTransaction();

$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->commit();
});
}

public function testRollbackFailure(): void
{
$this->connection->beginTransaction();

$this->expectConnectionLoss(static function (Connection $connection): void {
$connection->rollBack();
});
}

private function expectConnectionLoss(callable $scenario): void
{
if (! $this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) {
self::markTestSkipped('Restricted to MySQL.');
}

$this->connection->executeStatement('SET SESSION wait_timeout=1');
$this->connection->beginTransaction();

// during the sleep MySQL will close the connection
sleep(2);

$this->killCurrentSession();
$this->expectException(ConnectionLost::class);

// prevent the PHPUnit error handler from handling the "MySQL server has gone away" warning
Expand All @@ -65,6 +68,31 @@ private function expectConnectionLoss(callable $scenario): void
}
}

private function killCurrentSession(): void
{
$this->markConnectionNotReusable();

$databasePlatform = $this->connection->getDatabasePlatform();

[$currentProcessQuery, $killProcessStatement] = match (true) {
$databasePlatform instanceof AbstractMySqlPlatform => [
'SELECT CONNECTION_ID()',
'KILL ?',
],
$databasePlatform instanceof PostgreSQLPlatform => [
'SELECT pg_backend_pid()',
'SELECT pg_terminate_backend(?)',
],
default => self::markTestSkipped('Unsupported test platform.'),
};

$privilegedConnection = TestUtil::getPrivilegedConnection();
$privilegedConnection->executeStatement(
$killProcessStatement,
[$this->connection->executeQuery($currentProcessQuery)->fetchOne()],
);
}

public function testNestedTransactionWalkthrough(): void
{
if (! $this->connection->getDatabasePlatform()->supportsSavepoints()) {
Expand Down

0 comments on commit 598b2a3

Please sign in to comment.