-
Notifications
You must be signed in to change notification settings - Fork 11.2k
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
[10.x] After commit callback throwing an exception causes transaction level to be incorrect #50087
Conversation
…on level to be incorrect
I dunno - is this causing many real world problems? If so, please describe them from your own application and re-submit the PR. This code seems to be pretty fragile and if a lot of community members aren't raising serious bugs with it I would rather not touch it. Can you share more practical problem other than asserting against the internal transaction level and re-submit if necessary? |
@taylorotwell It is causing problems, otherwise I wouldn't be submitting this PR 🤷🏻♂️ . Some context: we're a large project that has 20k tests, nearly all of which use The reason the community doesn't seem to be aware of this is because it's a niche bug in a niche feature: it's only reproducible in tests with The internal transactions level is very important to be kept in full sync with the actual connection's level. Not doing so causes things not being committed, or being committed at a wrong time, or rolling back the transaction when not expected. For example: class SomeTest extends TestCase
{
use DatabaseTransactions;
public function testSomething()
{
$this->assertSame(1, DB::transactionLevel());
try {
DB::transaction(function () {
$this->assertSame(2, DB::transactionLevel());
DB::afterCommit(fn () => throw new \RuntimeException());
});
} catch (\RuntimeException) {}
// Incorrect transaction level here
// That's also the reason why `DatabaseTransactions` trait is important: without it the transaction level would
// actually be 0 by the time `afterCommit` callbacks even execute
$this->assertSame(0, DB::transactionLevel());
// Case 1:
// Instead of creating a savepoint, this attempts to create a transaction, which fails with a PDO exception:
// PDOException: There is already an active transaction
DB::beginTransaction();
// Case 2:
// Instead of making an actual "COMMIT;" query, this will silently not do anything, thinking it's outside of a
// transaction. Nothing will be committed, as the connection believes the transaction level to be 0.
DB::commit();
// Case 3:
// Similar to the commit case, this won't actually do anything as it believes the connection is at transaction level 0.
DB::rollBack();
}
} Obviously we generally don't use transactions directly in tests, but integration tests do call real endpoints/commands which do use transactions, which causes tests to fail. The real use case looks more like this: class SomeTest extends TestCase
{
use DatabaseTransactions;
public function testSomething()
{
$this->getJson('some_endpoint_with_after_commit_error')
->assertUnprocessable()
->assertJson(['code' => 'something']);
// Fails immediately due to `PDOException: There is already an active transaction`
$this->getJson('some_endpoint_with_any_transaction')
->assertOk();
}
} |
Hey.
Whenever an
afterCommit
callback throws an exception inside of aDB::transaction()
closure, it's caught by theManagesTransactions
trait and internal->transactions
transaction level is decremented. In tests that useDatabaseTransactions
trait this makesDB::transactionLevel()
return 0, although the real transaction level (on the database side) is still 1.This PR fixes this by running after commit callbacks outside of the try-catch, next to the "committing" event.