-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Fix incorrect handling of transactions using deferred constraints #3424
Conversation
Not sure why the continuousphp build failed - I think it's not related to my changes. Travis's MySQL died for some reason, here is the build of this branch against my fork: https://travis-ci.org/grongor/dbal/builds/475348248 |
lib/Doctrine/DBAL/Connection.php
Outdated
} catch (Exception $e) { | ||
$this->rollBack(); | ||
throw $e; | ||
} catch (Throwable $e) { | ||
$this->rollBack(); | ||
throw $e; | ||
} | ||
|
||
$this->commit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't a crash on commit()
lead to a rollback as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is precisely the issue here - when you call commit()
and it fails, then the transaction is no more :) There is nothing to rollback ... so this PR is all about not calling the rollback if the failure occurs in the commit()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then the transaction is no more
Ok, this is my misconception then. Are we sure that this holds consistently for all platforms that we support?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK all other platforms throw the exceptions before commit()
(they do not support constraint deferring), so we will always call rollback in those cases. But that might not be true - I'm not familiar enough with all of them to be perfectly honest.
Anyway, shouldn't the existing tests cover that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't the existing tests cover that?
If they run on all platforms, yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a case where the Exception happens during an open transaction? Shouldn't this improvement be marked as a BC break, when there are no rollbacks anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SenseException I'm sorry, I don't understand. I don't think that I changed the behavior for "common" transactions in any way; please provide an example code if you think so - thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You fixed your issue "Rollback called when outside of a transaction" in this method by moving the commit()
call out of the try-catch. Before your change an exception thrown in commit()
would call a rollback in the catch, but now this isn't the case anymore. What if commit()
throws an exception inside a transaction and a rollback is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, yeah, I understand now.
What if commit() throws an exception inside a transaction and a rollback is needed?
Then that case wouldn't be handled. The thing is that I was not able to come up with an example where COMMIT
would throw an exception and also where would need to call ROLLBACK
afterwards. I asked all devs in my company and no one was able to think of a situation like that. I also asked one guy who knows basically everything around MySQL and he was not able to think of such example (even tried poking master/slave and cluster setups).
But I guess unless someone checks all source codes/documentations of all supported databases we can't be 100 % sure. All I can do is to deploy those changes to our production systems (Postgres and MySQL) and report back if we experienced anything unusual...
Or can you think of a better solution to this problem?
lib/Doctrine/DBAL/Connection.php
Outdated
$this->commitDone(); | ||
} | ||
|
||
private function commitDone() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
: void
Also: naming suggests that committing finished, yet the code below begins a new transaction now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documentation block also needed - or better/more explicative naming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I wasn't sure about that part at all :D Do you have any suggestions for the name? It basically just resets the internal state of the object, after the commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is about resetting state, then the call to ->beginTransaction()
should be removed and replaced with direct state modification.
I don't have a suggestion for the name, because I don't understand this part of the diff myself either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Purpose of this method is to set the Connection
to the correct state. Until now the code didn't expect the commit()
to ever throw any exception. Now that it is possible we need to manage the state so that if someone catches the exception, then they can still use the connection afterwards. Without doing that the connection would be stuck with transactionNestingLevel
set to 1 even though the transaction is long gone. The ->beginTransaction()
is there because of the autoCommit
thingie - without it the Connection
object would again be in an unexpected state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, and that isn't clear by reading the method: can we somehow improve that?
Consider comments to be the last resort: having a clear method name would be a good improvement at first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Ocramius added the patch. Does updateTransactionStateAfterCommit()
make more sense?
lib/Doctrine/DBAL/Connection.php
Outdated
@@ -1172,15 +1172,17 @@ public function transactional(Closure $func) | |||
$this->beginTransaction(); | |||
try { | |||
$res = $func($this); | |||
$this->commit(); | |||
return $res; | |||
} catch (Exception $e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we catch Exception
and Throwable
in one catch here? Or Throwable
only?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to do more changes than was really necessary in this PR, but yeah, I think catching Throwable
only is a sensible and possible. Should I add the patch to this PR?
*/ | ||
public function testCommitWithDeferredConstraintAndTransactionNesting() : void | ||
{ | ||
$this->expectException(DBALException::class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent possible future BC breaks, I suggest to put expectException()
before the line, that throws the actual exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I usually do it like that but all tests in DBAL seemed to start with it, so I just went with it ... I'll update the code shortly
lib/Doctrine/DBAL/Connection.php
Outdated
} catch (Exception $e) { | ||
$this->rollBack(); | ||
throw $e; | ||
} catch (Throwable $e) { | ||
$this->rollBack(); | ||
throw $e; | ||
} | ||
|
||
$this->commit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a case where the Exception happens during an open transaction? Shouldn't this improvement be marked as a BC break, when there are no rollbacks anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a test which covers the change on all platforms, not only on those which support deferred constraints.
{ | ||
parent::setUp(); | ||
|
||
if (! in_array($this->connection->getDatabasePlatform()->getName(), ['postgresql', 'oracle'], true)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit scary to make a change to a core function and then test it only on two platforms. This specific test will require the platform to support deferrable constraints, but can a commit fail for a different generic reason which is applicable to all platforms?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov Yeah, I understand the fear, trust me :D I don't see another way to do it - how would I test something that the other platforms don't support? I'm not aware of a way to do that ... I think it must be sufficient that these changes don't break the existing tests as those tests should cover all the core/common functionality. And just to be extra sure I adjusted the ConnectionTest
to let the database throw the errors, you can check it out here #3425 .
And as far as I know the commit won't fail under a normal operation (other then connection lost, etc.). And even if it would fail, I think it would make sense that the transaction would be closed afterwards (what else the DB can expect, if we tried to commit and failed?). But yeah, I would love to know that for sure and add some tests for this :D But I can't think of any situation that would trigger error on commit (other then the deferred transactions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a build on top of that improved ConnectionTest
https://travis-ci.org/grongor/dbal/builds/479358748 - passing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how would I test something that the other platforms don't support? I'm not aware of a way to do that ...
Except for not being able to create a deferred constraint, how is the new test expected to behave on a platform that doesn't support deferred constraint? I.e. instead of skipping the test entirely, can we just create the deferred constraint if the platform supports it, and otherwise create a regular one?
I think it must be sufficient that these changes don't break the existing tests as those tests should cover all the core/common functionality.
Could you identify these tests?
@morozov I think that we do have a test that covers it on all platforms: the functional |
/** | ||
* @see https://github.com/doctrine/dbal/issues/3423 | ||
*/ | ||
class GH3423Test extends DbalFunctionalTestCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please avoid using issue numbers in tests. It should be named after the feature/behavior being tested.
@danydev, if fixing this bug doesn't require breaking changes, this PR may be retargeted against At the moment, there's no enough confidence that changing the code at the abstraction level and testing it only with certain platforms is sufficient.
You can help by participating in the conversation https://github.com/doctrine/dbal/pull/3424/files#r436288581. |
So ... I tried rebasing it today (4.x), and just running the tests against the current implementation. Here is the build: https://github.com/grongor/dbal/actions/runs/646044144 Postgres is broken: the constraint violation is completely throw away and replaced with "There is no active transaction". So ... do you want to continue this somehow, or should we close? I checked the tests around the Connection class and it seems they are a bit too synthetic. We could add a test that would check the constraint violation (without deferring), and if the new implementation won't break it, I think it's safe to say it will fix this issue without causing another. Would that be enough for this to get merged? If so, I'll update the PR. ps: Sorry for the "late response", it cost me a lot of time and I got tired at some point. We also stopped using the deferred constraints so it wasn't a priority for me anymore... |
That's the problem. The code being modified works for all platforms.
What do you want? If you want to close it, feel free to. If you want to proceed, you'll need to gather more information. Likely, not all platforms will behave the same, and the code change will become more complex. E.g. here (just as a reference) it's stated that a rollback is needed. And here is an example of using
What do you mean? |
No, it does not. See the link I provided. It doesn't work for the deferred constraints.
Nope. To quote exactly:
There isn't a single test that would check how the transaction behaves when there is an error, like for example violation of a constraint. By an error, I mean something that is triggered by the database, not something we throw explicitly in the test.
Nothing really. I just didn't want to be that guy that just closes the issue without giving it at least one last try. I've given this issue (and the one in ORM package) a lot of time already, and I only met with resistance, which to be honest I understand and think is warranted. I just expected someone to meet me half-way. I'm not gonna spend another 20 hours just to learn that "we aren't quite confident yet" - I hope you can understand that. Maybe there is another way to approach this. I wanted to remove the |
It's kind of hard to guarantee that a rollback is not needed after a failed commit in all vendors. For example sqlite may return a SQLITE_BUSY exception, and after that you still need to rollback. I think your second proposal may be the way to sort this out. Maybe It's not the cleanest, but it's the safer. |
Yeah I agree. To be honest I am quite surprised I didn't come up with it sooner lol, now it seems like an obvious way to do it. |
dabc8ac
to
7db0d8b
Compare
7db0d8b
to
39c25c5
Compare
@morozov what branch should this target? |
I've retargeted to |
109efc6
to
e805a0f
Compare
Co-authored-by: Simon Podlipsky <simon@podlipsky.net>
e805a0f
to
8b8f169
Compare
Summary
Rollback called when outside of a transaction when using deferred constraints (PostgreSQL).