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 handling of transactions using deferred constraints #4846

Draft
wants to merge 1 commit into
base: 4.0.x
Choose a base branch
from

Conversation

simPod
Copy link
Contributor

@simPod simPod commented Oct 4, 2021

Closes #3424 as it's very long and outdated so I decided to start again here

Q A
Type bug/feature
BC Break yes
Fixed issues #3423, doctrine/orm#7545

Summary

Rollback is being called when outside of a transaction when using e.g. deferred constraints or when using ORM and database throws error like https://www.cockroachlabs.com/docs/v21.1/transaction-retry-error-reference.html#retry_write_too_old which is then effectively hidden after PDO's There's no active transaction.

@simPod simPod force-pushed the df-c branch 8 times, most recently from cded7d3 to 82ce962 Compare October 4, 2021 11:38
@simPod simPod marked this pull request as ready for review October 4, 2021 13:02
@morozov
Copy link
Member

morozov commented Oct 8, 2021

@simPod thank you for the patch. I remember looking into this issue a couple of years ago. IIRC, it wasn't something trivial, so it may take some time before I post the review. But rest assured, it won't be ignored.

@simPod
Copy link
Contributor Author

simPod commented Oct 9, 2021

Thank you, note taken. Take your time.

Copy link
Member

@morozov morozov left a comment

Choose a reason for hiding this comment

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

I'm still wrapping my head around the changes but see a few comments below.

src/Connection.php Show resolved Hide resolved
src/Connection.php Outdated Show resolved Hide resolved
src/Driver/PDO/Connection.php Outdated Show resolved Hide resolved
[$firstMessage, $secondMessage] = explode("\n", $message, 2);

[$code, $message] = explode(': ', $secondMessage, 2);
$code = (int) str_replace('ORA-', '', $code);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to replace the error code obtained from the driver with the one parsed from the error message?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's the underlying one that interests us (UC violation). OCI chains errors so there are two errors returned
in this case. 2091 and 1.

But the code from the driver is related only to the first error (transaction roll back 2091) while we are interested in the second one to get information about the actual underlying issue (uc violated 1). We then know we can create UCViolation exception.

Copy link
Member

Choose a reason for hiding this comment

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

The unique constraint violation error is of interest to us in this specific case but there may be other cases. I don't think that replacing the error reported by the driver based on a personal preference is a good idea.

Instead, such errors could be represented as a chain (see Exception::$previous) of exceptions. So it would be 1) a transaction exception caused by 2) a unique constraint violation.

Copy link
Contributor Author

@simPod simPod Nov 20, 2021

Choose a reason for hiding this comment

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

I have to introduce custom TransactionRolledBack exception

Copy link
Contributor Author

@simPod simPod Nov 20, 2021

Choose a reason for hiding this comment

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

How do I make it work with ExceptionConverter then? It does not support chained errors from the driver.

This is what I have since I'm able to read multiple errors from the server now:

Error - Transaction rolled back <- Error - UC violation


This is what exception converter does right now

Driver Exception (Error - Transaction rolled back <- Error - UC violation)


We need to somehow inject UniqueConstraintViolationException into 👆🏾 but cannot simply use previous as OCI Error is used as previous now.

See 4f6487e

Copy link
Member

Choose a reason for hiding this comment

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

How do I make it work with ExceptionConverter then? It does not support chained errors from the driver.

That problem sounds solvable to me.

Instead, such errors could be represented as a chain (see Exception::$previous) of exceptions. So it would be 1) a transaction exception caused by 2) a unique constraint violation.

This is the way.

$pdoException = new PDOException(<<<TEXT
OCITransCommit: ORA-02091: transaction rolled back
ORA-00001: unique constraint (DOCTRINE.C1_UNIQUE) violated
(/private/tmp/php-20211003-35441-1sggrmq/php-8.0.11/ext/pdo_oci/oci_driver.c:410)
Copy link
Member

Choose a reason for hiding this comment

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

Please do not add unit tests for such runtime-specific cases. If necessary, this logic should be tested in an integration way across all relevant platforms.

Copy link
Contributor Author

@simPod simPod Oct 21, 2021

Choose a reason for hiding this comment

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

It is tested in integration way in the main test here.

Wanted to clarify and validate the custom logic so I isolated the error message parsing behaviour into unit tests as well to better document it. You asked exactly about it here #4846 (comment) and this tests covers it. But I can remove it before merging for sure.

} elseif ($platform instanceof PostgreSQLPlatform) {
$constraintName = 'c1_unique';
} else {
$constraintName = 'c1_unique';
Copy link
Member

Choose a reason for hiding this comment

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

How is it different from the above? Is it possible to use strtolower() where necessary instead of branching based on the platform?

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 better to store constraint name in advance as it is not used for assertions only but in SQL statements as well

e.g. SET CONSTRAINTS "%s" DEFERRED

);
} elseif ($platform instanceof SqlitePlatform) {
$createConstraint = sprintf(
'CREATE UNIQUE INDEX %s ON deferrable_constraints(unique_field)',
Copy link
Member

Choose a reason for hiding this comment

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

So it looks like we're testing a feature that isn't supported by the DBAL. Does SQLite support deferrable constraints?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it does not. But we wanted to run the test on all platforms #3424 (review)

Copy link
Member

Choose a reason for hiding this comment

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

In this case, it would make more sense to have two tests: each covers its own feature on the platforms that support it.

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 renamed the test. The initial motive of previous author was to handle deferred constraints only in the original PR.

Then we realised unique constraints were not really tested so we've expanded the test. So this tests unique constraint violations while it also tests deferring the violations where eligible. So I don't really see how to split it but I've adapted the naming as it was misleading for sure.

final class DeferrableConstraintsTest extends FunctionalTestCase
{
/** @var string */
private $constraintName = '';
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 be part of the test state? It's the test data that should be static.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should I convert it to method then? I need to know the name of constraint for each platform (#4846 (comment))

@morozov
Copy link
Member

morozov commented Oct 20, 2021

Could you try rebasing so that all the required build jobs get executed?

@github-actions
Copy link

There hasn't been any activity on this pull request in the past 90 days, so it has been marked as stale and it will be closed automatically if no further activity occurs in the next 7 days.
If you want to continue working on it, please leave a comment.

@github-actions github-actions bot added the Stale label Jul 30, 2022
@simPod
Copy link
Contributor Author

simPod commented Sep 16, 2023 via email

@simPod
Copy link
Contributor Author

simPod commented Sep 16, 2023

Bot

@simPod
Copy link
Contributor Author

simPod commented Sep 16, 2023

Fix urself

@github-actions github-actions bot removed the Stale label Sep 17, 2023
@derrabus
Copy link
Member

Can somebody fill me in what's up with this PR? It does not look like the kind of change we would accept on the 4.0.x branch.

@simPod
Copy link
Contributor Author

simPod commented Nov 17, 2023

@derrabus see doctrine/orm#7545 Current behavior section. The problem is that we're trying to rollback already rollbacked transaction and therefore the underlying issue (like unique constraint violation) is suppressed by There's no active transaction.

@derrabus
Copy link
Member

Understood. Let me take over the review then. But as I said, we don't accept this kind of change on the 4.0.x branch. You need to target 3.7.x currently if this is a bugfix and 3.8.x otherwise.

@simPod
Copy link
Contributor Author

simPod commented Nov 17, 2023

Great. I have rebased this like trillion times over the years so I'm glad there's a promise of a movement now. I'll target 3.8.x I guess.

@simPod simPod changed the base branch from 4.0.x to 3.8.x November 20, 2023 11:03
@simPod simPod marked this pull request as draft November 20, 2023 21:03
@simPod simPod force-pushed the df-c branch 2 times, most recently from d94da30 to 7ac4705 Compare December 2, 2023 16:55
@simPod simPod marked this pull request as ready for review December 2, 2023 16:56
@simPod
Copy link
Contributor Author

simPod commented Dec 2, 2023

@derrabus this is now working again.

$code = $error['code'];
$message = $error['message'];
if ($code === self::CODE_TRANSACTION_ROLLED_BACK) {
// There's no way this can be unit-tested as it's impossible to mock $resource
Copy link
Member

Choose a reason for hiding this comment

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

This kind of comment does not belong into production code.

[$firstMessage, $secondMessage] = explode("\n", $message, 2);

[$code, $message] = explode(': ', $secondMessage, 2);
$code = (int) str_replace('ORA-', '', $code);
Copy link
Member

Choose a reason for hiding this comment

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

How do I make it work with ExceptionConverter then? It does not support chained errors from the driver.

That problem sounds solvable to me.

Instead, such errors could be represented as a chain (see Exception::$previous) of exceptions. So it would be 1) a transaction exception caused by 2) a unique constraint violation.

This is the way.

return new self($error['message'], null, $error['code']);
$code = $error['code'];
$message = $error['message'];
if ($code === self::CODE_TRANSACTION_ROLLED_BACK) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't feel comfortable with making this exception class aware of specific error codes. That logic belongs into ExceptionConverter. Can we replace this logic with a generic detection of chained errors?

Copy link

github-actions bot commented Mar 3, 2024

There hasn't been any activity on this pull request in the past 90 days, so it has been marked as stale and it will be closed automatically if no further activity occurs in the next 7 days.
If you want to continue working on it, please leave a comment.

@github-actions github-actions bot added the Stale label Mar 3, 2024
- Let root Errors bubble up
- Catch concrete Exception in TransactionTest as we can do it now
- Expose underlying errors on Oracle platform

Co-authored-by: Jakub Chábek <jakub.chabek@cdn77.com>
@simPod simPod changed the base branch from 3.8.x to 4.0.x March 4, 2024 10:32
@simPod simPod marked this pull request as draft March 4, 2024 10:35
@github-actions github-actions bot removed the Stale label Mar 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants