Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset nested transaction level on deadlock exceptions #6103

Closed
wants to merge 3 commits into from
Closed
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
8 changes: 8 additions & 0 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Doctrine\DBAL\Event\TransactionCommitEventArgs;
use Doctrine\DBAL\Event\TransactionRollBackEventArgs;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
Expand Down Expand Up @@ -1276,6 +1277,8 @@ public function transactional(Closure $func)
$this->commit();

return $res;
} catch (DeadlockException $e) {
throw $e;
} catch (Throwable $e) {
$this->rollBack();

Expand Down Expand Up @@ -1938,6 +1941,11 @@ private function handleDriverException(
$this->close();
}

if ($exception instanceof DeadlockException) {
//Reset transaction nesting level since deadlocks always rollback.
$this->transactionNestingLevel = 0;
}

return $exception;
}

Expand Down
77 changes: 77 additions & 0 deletions tests/Functional/Driver/PDO/MySQL/DeadlockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Doctrine\DBAL\Tests\Functional\Driver\PDO\MySQL;

use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;

use function implode;
use function pcntl_fork;
use function sleep;
use function sprintf;

/** @require extension pcntl */
class DeadlockTest extends FunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();

$supportedDrivers = ['pdo_mysql', 'mysqli', 'pdo_pgsql'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You said that this is an issue of MySQL drivers only, yet you're including PDO_pgsql as well. Does it make sense to exclude any drivers here? Can we come up with a driver-agnostic functional test that makes sure we can start a new transaction aftera deadlock?

Copy link
Author

@tzkoshi tzkoshi Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only mysql/postgres of the supported drivers have deadlock exceptions. Other databases do not have such exceptions.

But, it seems that after attempting what you have suggested, to open a new transaction, this fix isn't enough and the pdo driver library must first be patched, as it stays in "active transaction state".

See the link here:
https://github.com/php/php-src/blob/b132b7ab7efb40628dedf2eae1cf7d6949684bcb/ext/pdo/pdo_dbh.c#L605

For now will postpone that pull request and willl attempt to fix firstly the php pdo driver and then will modify that PR.

Sorry for bothering you at this time.

if (! TestUtil::isDriverOneOf(...$supportedDrivers)) {
self::markTestSkipped(sprintf('This supports one of %s drivers', implode(', ', $supportedDrivers)));
}

$table = new Table('test1');
$table->addColumn('id', 'integer');
$this->dropAndCreateTable($table);

$table = new Table('test2');
$table->addColumn('id', 'integer');
$this->dropAndCreateTable($table);

$this->connection->executeStatement('INSERT INTO test1 VALUES(1)');
$this->connection->executeStatement('INSERT INTO test2 VALUES(1)');
}

public function testNestedTransactionsDeadlockExceptionHandling(): void
{
$this->connection->setNestTransactionsWithSavepoints(true);

$this->connection->beginTransaction();
$this->connection->beginTransaction();
$this->connection->executeStatement('DELETE FROM test1');

$pid = pcntl_fork();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to fork the process? Wouldn't opening a second connection be enough to provoke a deadlock?

Copy link
Author

@tzkoshi tzkoshi Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, we can't as PHP is single threaded and if we attempt to simply work with two connections it just hangs till a timeout is reached. Already tried it.
Another possible solution to skip the output buffer of phpunit is to simply die in the child process, but for me it seems a bit "hacky", if that is the thing that bothers you.

if ($pid) {
$connection = TestUtil::getConnection();
$connection->beginTransaction();
$connection->executeStatement('DELETE FROM test2');
$connection->executeStatement('DELETE FROM test1');
$connection->commit();
$connection->close();

$this->markTestSkipped('Gracefully skip child process in deadlock test');
}

try {
$this->waitForTableLock();
$this->connection->executeStatement('DELETE FROM test2');
$this->connection->commit();
$this->connection->commit();
} catch (DeadlockException $ex) {
self::assertFalse($this->connection->isTransactionActive());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion does not tell me that the connection's transaction status is in sync with the driver.


return;
}

$this->fail('Expected deadlock exception did not happen.');
}

private function waitForTableLock(): void
{
sleep(5);
}
}
Loading