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

Not throwing proper exception when in transaction #7545

Closed
simPod opened this issue Dec 28, 2018 · 14 comments
Closed

Not throwing proper exception when in transaction #7545

simPod opened this issue Dec 28, 2018 · 14 comments
Labels

Comments

@simPod
Copy link
Contributor

simPod commented Dec 28, 2018

Bug Report

Q A
Version 2.6.3

Summary

EntityManager tries to rollback the transaction when exception is thrown during commit operation. The rollback is performed through DBAL Connection. The problem is that the transaction is no longer active and when DBAL Connection tries to rollback through PDOConnection, There is no active transaction exception is thrown instead and the original exception is lost. So user has no easy way to know actual exception and gets There is no active transaction instead.

Current behavior

  1. PDOException A SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint ... occurs on commit in https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/EntityManager.php#L238
  2. Transaction is attempting to rollback https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/EntityManager.php#L243 before exception A is thrown on https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/EntityManager.php#L245
  3. DBAL Connection::rollback() is called where $this->_conn->rollBack(); is called. _conn is instance of PDOConnection that throws There is no active transaction exception instead and the original exception is lost.

Expected behavior

The original exception is properly thrown.

@Ocramius
Copy link
Member

The original exception is properly thrown.

I'd rather say that the failed rollback should have priority here, as something much worse is happening (no active transaction in first place).

@Ocramius Ocramius added the Bug label Dec 28, 2018
@simPod
Copy link
Contributor Author

simPod commented Dec 28, 2018

I forgot to mention that the whole operation is called within $entityManager::transactional() but that can be seen in linked source.

I see that it starts with $this->conn->beginTransaction();
Followed by $this->conn->commit() after flush and $this->_conn->commit() within it.

Just checked that before calling PDOConnection::commit(), the transaction is active PDO::inTransaction() -> true

After first PDOException with Unique violation is thrown, the transaction is no longer active.

Most possibly because $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); is set on DBAL PDOConnection in the constructor so commit() throws exception and cancels transaction.

@simPod
Copy link
Contributor Author

simPod commented Dec 28, 2018

I decided to go with a test. Found out, that it works just fine. The problem is that in some cases, I defer unique constrains (eg. when doing full sync, running all CUD operations)

When constraint is deferred, the violation is thrown during commit, not on flush and therefore the issue pops up.

https://github.com/simPod/doctrine2/commit/cd031c84b7346fc264819b211e51f5d00408cc94

Tests pass when line :48 is commented out (constraint defer is disabled).

@Ocramius
Copy link
Member

Ocramius commented Jan 2, 2019

@simPod I'd suggest tackling this within 3.x, by adding a better exception type that captures both rollback and previous failures.

@simPod
Copy link
Contributor Author

simPod commented Jan 5, 2019

@grongor took over 👋

@ro0NL
Copy link

ro0NL commented Apr 26, 2024

the original exception should not/never be swallowed, and it's a real prod debug issue

we'll get an exception either way, i prefer the original one

so i suggest tackling this within 2.x

@greg0ire
Copy link
Member

@ro0NL this was reported 6 years ago. Can you please provide a full stack trace of the exception you are getting now? I would like to know if it is still a PDOException or if it is wrapped in another exception type. If we could with Marco's solution, I think changing the exception type could be considered a breaking change, even though I don't see why anyone would want to catch something this serious. If that's the case, I would suggest cloning the exception, and tweaking the message of the cloned exception so that it contains a string representation of the previous exception, or adding a property to the exception so that it's possible for an error handler to display it however they see fit. I don't think we will be able to use $previous here.

@ro0NL
Copy link

ro0NL commented Apr 27, 2024

Doctrine\DBAL\Exception\DriverException

An exception occurred while executing a query: SQLSTATE[42000]: Syntax error or access violation: 1305 SAVEPOINT DOCTRINE_2 does not exist

/app/vendor/doctrine/dbal/src/Connection.php:1943
/app/vendor/doctrine/dbal/src/Connection.php:1885
/app/vendor/doctrine/dbal/src/Connection.php:1213
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:292
/app/vendor/doctrine/dbal/src/Connection.php:1638
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:365
/app/vendor/doctrine/dbal/src/Connection.php:1535
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:322
/app/vendor/doctrine/orm/src/UnitOfWork.php:485
/app/vendor/doctrine/orm/src/EntityManager.php:403

previous: Doctrine\DBAL\Driver\PDO\Exception

SQLSTATE[42000]: Syntax error or access violation: 1305 SAVEPOINT DOCTRINE_2 does not exist

/app/vendor/doctrine/dbal/src/Driver/PDO/Connection.php:39
/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php:46
/app/src/Bundles/CoreFunctionsBundle/Dbal/Connection.php:90
/app/vendor/doctrine/dbal/src/Connection.php:1211
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:292
/app/vendor/doctrine/dbal/src/Connection.php:1638
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:365
/app/vendor/doctrine/dbal/src/Connection.php:1535
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:322
/app/vendor/doctrine/orm/src/UnitOfWork.php:485
/app/vendor/doctrine/orm/src/EntityManager.php:403

previous previous: PDOException

SQLSTATE[42000]: Syntax error or access violation: 1305 SAVEPOINT DOCTRINE_2 does not exist

/app/vendor/doctrine/dbal/src/Driver/PDO/Connection.php:33
/app/vendor/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php:46
/app/src/Bundles/CoreFunctionsBundle/Dbal/Connection.php:90
/app/vendor/doctrine/dbal/src/Connection.php:1211
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:292
/app/vendor/doctrine/dbal/src/Connection.php:1638
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:365
/app/vendor/doctrine/dbal/src/Connection.php:1535
/app/vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php:322
/app/vendor/doctrine/orm/src/UnitOfWork.php:485
/app/vendor/doctrine/orm/src/EntityManager.php:403

@greg0ire
Copy link
Member

greg0ire commented Apr 27, 2024

Then I think a possible design could be defining DriverException::withPrevious(\Throwable $throwable) or rather DriverException::withSomeOtherName() to avoid confusion with $previous, add a getter for that thing, then modify Connection::rollback() so that it accepts a throwable, and calls the wither above when that throwable is defined and an exception occurs here: https://github.com/doctrine/dbal/blob/4.0.x/src/Connection.php#L1061-L1063

Then, that extra exception could be displayed by the error handler… that could be an issue since it would mean the error handler would have to know about the wither. Another solution to that could be to clone the exception and modify its message.

Sadly, this means this cannot be tackled in 2.x

@ro0NL
Copy link

ro0NL commented Apr 27, 2024

perhaps simply rethrow with original exception concatted in message?

https://3v4l.org/FmcJa

@greg0ire
Copy link
Member

greg0ire commented Apr 27, 2024

Yes, that's what I mean by "modify its message" (except I would use deep cloning). In the meantime, I think there is something you could try on your application to debug your urgent production issue with ORM 2 and DBAL 3: writing a DBAL middleware that catches, logs and rethrows any exception it sees go through.

Note that since AbstractConnectionMiddleware has a rollback method, you could go as far as memoizing the exception, and then logging when and only when rollback() is called and an exception occurs when calling $this->wrapped->rollback().

rodnaph added a commit to owsy/orm that referenced this issue Jun 1, 2024
these are caused by postgres constraint issues, and are not currently
handled well by doctrine:

doctrine#7545
@adlpz
Copy link

adlpz commented Jun 20, 2024

We've been seeing this issue. I'll add some context on what we've found:

  • We're in ORM 2.
  • The DB is MySQL and we're using savepoints.
  • In our case, the underlying exception is a Doctrine\DBAL\Exception\DeadlockException.
  • Apparently with MySQL an implicit rollback happens when a deadlock occurs.
  • The exception is triggered by a $em->flush() which calls $uow->commit(). In the main try/catch block of the method $conn->isTransactionActive() is true and $conn->rollBack() is called.
  • If we don't use savepoints, this results in a ROLLBACK issued, which with no transactions active (they've been implicitly rolled back) is a no-op.
  • However with savepoints this results in a ROLLBACK TO SAVEPOINT DOCTRINE_X. As this savepoint doesn't exist anymore, a Doctrine\DBAL\Exception\DriverException is thrown.
  • The same catch block handles the new exception which overwrites the original from then on. The original issue never reaches our logging infrastructure.

We've seen two possible workarounds:

  • Forking/patching UnitOfWork to add a try-catch around the rollback.
  • Using a connection middleware to patch $connection->exec() and $connection->rollBack() and catch/log any exceptions from these statements.

However in any case a proper solution for this issue would be, as suggested, to include the information of the previous exception somehow. Maybe with a specific exception class that handles exceptions happened during the handling of an exception like it's the case here.

@greg0ire
Copy link
Member

greg0ire commented Oct 9, 2024

Hello from the doctrine hackathon 👋

I discussed this with the team, and @alcaeus told me that code inside a catch block should never fail. If it does, it might be because we are trying to do something despite the fact that an exception occurred. That kind of stuff should be done in a finally block, and as it turns out, when you use this pattern, PHP handles it gracefully and displays both exceptions 🤯
So the fix is going to be simpler than expected I think.

https://3v4l.org/9QiaO

@greg0ire
Copy link
Member

greg0ire commented Oct 9, 2024

Looks like Symfony handles it properly as well

Screenshot 2024-10-09 at 14-56-46 b (500 Internal Server Error)

greg0ire added a commit to greg0ire/doctrine-orm that referenced this issue Oct 9, 2024
catch blocks are not supposed to fail. If you want to do something
despite an exception happening, you should do it in a finally block.

Closes doctrine#7545
greg0ire added a commit to greg0ire/doctrine-orm that referenced this issue Oct 10, 2024
catch blocks are not supposed to fail. If you want to do something
despite an exception happening, you should do it in a finally block.

Closes doctrine#7545
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants