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

Fix incorrect transactional() handling when DB auto-rolled back the transaction #6545

Open
wants to merge 2 commits into
base: 3.9.x
Choose a base branch
from

Conversation

simPod
Copy link
Contributor

@simPod simPod commented Oct 12, 2024

Q A
Type bug
Fixed issues #3423

Summary

#6543 partially solved what we tried to achieve in #4846. With that There's no active transaction exception contains the actual exception in $previous.

Now, let's get rid of There's no active transaction exception which occurs e.g. when using deferred constraints so the violation is checked at the end of the transaction and not during it. Database then rolls back the transaction automatically.

The current faulty flow is following:

  1. on commit(), Constrain violation occurs
  2. transaction is implicitly aborted and rolled back in database
  3. DBAL calls rollback()
  4. Since there's no active transaction, There's no active transaction exception is thrown:
image

This PR adjusts the flow so:

  1. on commit(), Constrain violation occurs
  2. transaction is implicitly aborted and rolled back in database
  3. DBAL DOES NOT call rollback() 🥳

For DBAL v4 it looks like this #6546 (draft, does not contain review changes)

@simPod simPod marked this pull request as ready for review October 12, 2024 12:17
} finally {
if (! $successful) {
$this->rollBack();
}
}

$this->commit();
Copy link
Member

Choose a reason for hiding this comment

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

What if commit throws for some other reason, before anything is transmitted to the database?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you describe some scenario you have in mind so I can cover it with test?

Copy link
Member

Choose a reason for hiding this comment

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

Here, there are throws statements:

dbal/src/Connection.php

Lines 1013 to 1026 in 220aec4

throw NoActiveTransaction::new();
}
if ($this->isRollbackOnly) {
throw CommitFailedRollbackOnly::new();
}
$connection = $this->connect();
if ($this->transactionNestingLevel === 1) {
try {
$connection->commit();
} catch (Driver\Exception $e) {
throw $this->convertException($e);

I don't know if there is a realistic scenario in which they can throw when commit is called from the method you are modifying, before the transaction is actually committed, but that's what I was thinking about.

Copy link
Contributor Author

@simPod simPod Oct 12, 2024

Choose a reason for hiding this comment

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

I see. Those should not happen but I have adjusted the flow and covered with unit test should it happen.

72af9b8

simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
@simPod simPod requested a review from greg0ire October 12, 2024 14:48
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
src/Connection.php Show resolved Hide resolved
if ($shouldRollback) {
$this->rollBack();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Why not do something similar to what I did in doctrine/orm@b6137c8 and use isTransactionActive()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot about that API, should work.

Copy link
Member

Choose a reason for hiding this comment

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

What should happen in this scenario?

$connection->beginTransaction();
$connection->transactional(some function call here);
$connection->commit();

I'm not familiar with nested transactions, but if it's possible to nest them then this will return true because of the outer transaction, although the outer transaction will have been property closed. Also, why not reuse the previous try / finally block just like I did in ORM?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nested transactions should be covered by 4 tests.

I merged try blocks, it makes sense and is nice with the isTransactionActive API.

Copy link
Member

Choose a reason for hiding this comment

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

Now the code looks like it is going to wrongly revert the outer transaction after successfully committing the inner one, just because the outer one is active (as it should)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Umm I'm confused now. Will take a break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can add a check of nesting level but the green test suite somehow implies it won't happen at all anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It indeed tried to rollback the transaction. I have reverted to a previous version and added a test to cover the case.

Copy link
Member

Choose a reason for hiding this comment

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

I am confused now. Are you saying that the bug exists, and that you reproduced it? I don't see any check on the transaction nesting level, which does sound exactly like what would be needed here, so why is the tests suite mostly green (it's red on Oracle, do you know why?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's red on Oracle, do you know why

oracle and db2 require select... to have also from clause. Fixed.

Are you saying that the bug exists, and that you reproduced it

Yes, the bug would have been introduced with 88879b3. I covered it with testNestedTransactionWalkthrough() so we know it passes.

When doing commit(), we need to rollback the transaction only if something failed outside the database #6545 (comment).

Since we need to have rollback() in finally, we need to use boolean flag to know whether to trigger it.
We can't use isTransactionActive() since it only checks the nesting level. isTransactionActive() can actually be renamed to something isTransactionNested().

@greg0ire
Copy link
Member

It seems there are CI jobs failing. Please take a look at this guide for more on how to handle those.

@simPod
Copy link
Contributor Author

simPod commented Oct 12, 2024

Thanks, just did not notice that basic stuff, u were faster 🙃 fixed.

simPod added a commit to simPod/dbal that referenced this pull request Oct 12, 2024
@simPod simPod force-pushed the df-c_v3 branch 3 times, most recently from 71e5b43 to 9879edf Compare October 13, 2024 10:15
@simPod simPod requested a review from greg0ire October 13, 2024 10:19
@simPod simPod force-pushed the df-c_v3 branch 2 times, most recently from ebc4aa1 to b512770 Compare October 13, 2024 11:20
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
}
} finally {
$this->updateTransactionStateAfterCommit();
Copy link
Member

Choose a reason for hiding this comment

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

If the above lines are successful but something goes wrong here, then $shouldRollback will be true (right?) although no rollback should happen.

That's why I was expecting some code that takes a look at the transaction nesting level before and after the commit. If the transaction level decreased all by itself.

Looking at the code though, I don't see how it could fail, so I guess this is OK.

Copy link
Member

Choose a reason for hiding this comment

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

It's unclear to me why you are calling this function inside a finally block. If the commit fails, why should the transaction nesting level decrease?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the commit fails, why should the transaction nesting level decrease?

We need to cleanup the transaction state. Otherwise isTransactionActive() will still report true to other functions (e.g. in disconnect()) which is not true actually. It is covered by UniqueConstraintViolationsTest.

Copy link
Member

Choose a reason for hiding this comment

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

Why should it no longer be considered active? If COMMIT fails, then the transaction will be "aborted", but it won't be auto-rolled-back, right? So to me, I'd say the transaction is still going on, but it is aborted and unusable, and to regain control, you have to use ROLLBACK TO or ROLLBACK.

That's what I understand after reading the last few sentences of https://www.postgresql.org/docs/current/tutorial-transactions.html anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, good point.

I came up with the solution where we can check the exception type and if it is a known case where the transaction is auto rolled back, we won't call ROLLBACK.

36e23d1

@simPod
Copy link
Contributor Author

simPod commented Oct 19, 2024

yes, of course. I meant "how the wrong rollback" can happen in this code flow.

Let's say the network error happens and the opening of the transaction fails. An exception will bubble up, preventing $successful from switching to true, and there will be an attempt to rollback a transaction that was never happened in the first place. Right?

I think I covered the case where

transactional() is used here

https://github.com/doctrine/dbal/pull/6545/files#diff-a3bde44a40c3047e1a8c316dd4e5dbb1c8f8d169fa9558e01faabbb2a794d58cR400-R440

and where transactional() is not used here

https://github.com/doctrine/dbal/pull/6545/files#diff-a3bde44a40c3047e1a8c316dd4e5dbb1c8f8d169fa9558e01faabbb2a794d58cR459-R486


When autocommit=false, then calling transactional() increases nesting level to 2 and this line is never called

therefore, there does not appear to be any attempt to rollback 🤔

@greg0ire
Copy link
Member

When autocommit=false, then calling transactional() increases nesting level to 2 and this line is never called
therefore, there does not appear to be any attempt to rollback 🤔

Makes sense to me. What about when autocommit=true? Sorry if this should be obvious to me, but I don't have all the details loaded in mind.

@simPod
Copy link
Contributor Author

simPod commented Oct 19, 2024

When autocommit=false, then calling transactional() increases nesting level to 2 and this line is never called
therefore, there does not appear to be any attempt to rollback 🤔

Makes sense to me. What about when autocommit=true? Sorry if this should be obvious to me, but I don't have all the details loaded in mind.

Absolutely understand.

I have covered calling transactional() with and without autocommit in those tests.

What about when autocommit=true

When it's true it does not satisfy the left side of the condition in order to rollback

https://github.com/simPod/dbal/blob/e6cf572874bf74f816b94f034be9440aed1c8d76/src/Connection.php#L1482

If it's false it does not satisfy the right side because

When autocommit=false, then calling transactional() increases nesting level to 2 and this line is never called.

@simPod
Copy link
Contributor Author

simPod commented Oct 19, 2024

I think I have addressed @/derrabus' (not tagging explicitly to avoid noise) comments from previous attempt related to OCI and PDO_OCI #4846 (review)

tests/ConnectionTest.php Outdated Show resolved Hide resolved
tests/ConnectionTest.php Outdated Show resolved Hide resolved
tests/Functional/ForeignKeyConstraintViolationsTest.php Outdated Show resolved Hide resolved
@greg0ire
Copy link
Member

greg0ire commented Oct 20, 2024

Please use an interactive rebase to make your commits atomic (all commits should be "revertable" / pass the build). Also, if you can split this PR, that would be great.

@simPod
Copy link
Contributor Author

simPod commented Oct 20, 2024

Yup, though for the review process I wanted the commits to represent addressing the comments / explain the issue.

Will consolidate + continue extracting.

@simPod simPod changed the title Fix incorrect handling of transactions when using deferred constraints Fix incorrect transactional() handling when DB auto-rolled back the transaction Oct 20, 2024
… transaction

Let's get rid of There's no active transaction exception which occurs e.g. when using deferred constraints so the violation is checked at the end of the transaction and not during it.

- Do not rollback only on exceptions where we know the transaction is no longer active
- Introduce TransactionRolledBack exception
- Transform Oracle's "transaction rolled back" exception and use the underlying one that DBAL supports
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants