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

RFC: Prevent UnitOfWork::commit() reentrance? #10900

Closed
wants to merge 1 commit into from

Conversation

mpdude
Copy link
Contributor

@mpdude mpdude commented Aug 9, 2023

This PR prevents reentrant calls to UnitOfWork::commit() with a clear exception message.

Reentrance happens, for example, when users call EntityManager::flush() from within postPersist or similar event listeners. Since #10547, it causes strange-looking, non-helpful error messages (#10869).

Instead of treating this as a bug and trying to fix it, my suggestion is to fail with a clear exception message instead.

Reasons:

I assume the UoW was never designed to support reentrant flush() calls in the first place. So, trying to make (or keep) this working might be a rabbit hole.

The assumption is based on the following:

  • Not a single test in the ORM test suite covers or even triggers flush() reentrance – otherwise, such a test would have failed with the changes suggested here.
  • Documentation for e. g. preFlush or postFlush explicitly mentions that flush() must not be called again. I don't know why this is not also stated for e. g. postPersist. But why would it be safe to call flush() during postPersist, in the middle of a transaction, when there are reasons against calling it in postFlush, just before final cleanups are made?
  • Last but not least, entity insertions, computed changesets etc. are kept in fields like UnitOfWork::$entityChangeSets or UnitOfWork::$originalEntityData. It's all to easy to mess up these states when we re-compute changesets mid-way through a flush() – and again, if that were anticipated, I'd assume to find any kind of test coverage.

TODO

@mpdude mpdude changed the title Prevent UnitOfWork::commit() reentrance RFC: Prevent UnitOfWork::commit() reentrance Aug 9, 2023
This PR prevents reentrant calls to `UnitOfWork::commit()` with a clear exception message.

Reentrance happens, for example, when users call `EntityManager::flush()` from within `postPersist` or similar event listeners. Since doctrine#10547, it causes strange-looking, non-helpful error messages (doctrine#10869).

Instead of treating this as a bug and trying to fix it, my suggestion is to fail with a clear exception message instead.

Reasons:

I assume the UoW was never designed to support reentrant `flush()` calls in the first place. So, trying to make (or keep) this working might be a rabbit hole.

The assumption is based on the following:

* Not a single test in the ORM test suite covers or even triggers `flush()` reentrance – otherwise, such a test would have failed with the changes suggested here.
* Documentation for e. g. [`preFlush`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/reference/events.html#preflush) or [`postFlush`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/reference/events.html#preflush) explicitly mentions that `flush()` must not be called again. I don't know why this is not also stated for e. g. `postPersist`. But why would it be safe to call `flush()` during `postPersist`, in the middle of a transaction, when there are reasons against calling it in `postFlush`, just before final cleanups are made?
* Last but not least, entity insertions, computed changesets etc. are kept in fields like `UnitOfWork::$entityChangeSets` or `UnitOfWork::$originalEntityData`. It's all to easy to mess up these states when we re-compute changesets mid-way through a `flush()` – and again, if that were anticipated, I'd assume to find any kind of test coverage.
@mpdude
Copy link
Contributor Author

mpdude commented Aug 9, 2023

@beberlei maybe you recall if this was ever meant to work?

@derrabus
Copy link
Member

Tests please. 🙂

@mpdude
Copy link
Contributor Author

mpdude commented Aug 10, 2023

☝🏻 – sure, as soon as we agree we want to go down that road. Converting PR to draft for the time being.

@mpdude mpdude marked this pull request as draft August 10, 2023 10:53
@mpdude mpdude changed the title RFC: Prevent UnitOfWork::commit() reentrance RFC: Prevent UnitOfWork::commit() reentrance? Aug 10, 2023
@jmperruchini
Copy link

jmperruchini commented Aug 11, 2023

I modified the UnitOfWork file, I now have the following error, if I comment out $this->entityManager->flush();, I don't have an error but
I don't have a database record.

The UnitOfWork is currently in the commit phase. You must not call UnitOfWork::commit() or EntityManager::flush() at this time, since commit phases cannot safely be nested.

LogicException
in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php (line 375)
in vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php -> commit (line 403)
EntityManager->flush() in src/Trace/Logger.php (line 42)

    if (isset($user)) {
        $log->setUtilisateur($user);
    }
    $log->setLibelleLog($libelle);
    $this->entityManager->persist($log);
    $this->entityManager->flush();
}

}

Logger->log('create', 'consultation', 7368, '127.0.0.1', '11/08/2023 14:24:26 : l'utilisateur Dr X (medecine physique et de readaptation - CEDEX) (ip : 127.0.0.1) a créé un objet dans le module consultation, id=7368 (action : create) - patient sélectionné : Mr X', object(SfGuardUser), object(Patient)) in src/Trace/Trace.php (line 183)

Should I wait for changes? or should I not try to create logs with postpersist? the problem is that I can't retrieve the id of newly created objects in my logs in onflush or other doctrine events!

mpdude added a commit to mpdude/doctrine2 that referenced this pull request Aug 17, 2023
This PR addresses the issue brought up in doctrine#10869. It happens when users use event listeners – `postPersist` in particular – to make changes to entities and/or persist new entities and finally call `EntityManager::flush()`, while the `UnitOfWork` is currently within the commit phase.

There is a discussion in doctrine#10900 whether this kind of reentrance should be deprecated in 2.x and prevented in 3.x. But, in order to prevent complete breakage with the 2.16.0 update, this PR tries to apply a band-aid 🩹.

A few things changed inside the UoW commit implementation in 2.16, and for sure this PR does not restore all details of the previous behavior. Take it with a grain of salt.

Here's the details.

The issue appears when `UoW::commit()` is performing entity insertions, and `postPersist` listener causes `commit()` reentrance when there are pending insertions left.

In that situation, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty.

The entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to the reported failure down the road. The mitigation is to check for this condition and skip such entities.

Before doctrine#10547 (pre-2.16), things worked a bit differently:

The UoW did not build a list of entities (objects) as the commit order, but a sequence of _classes_ to process.

For every entity _class_, it would find all entity instances in `UoW::$entityInsertions` and pass them to the persister in a batch. The persister, in turn, would execute the `INSERT` statements for _all_ those entities _before_ it dispatched the `postInsert` events.

That means when a `postInsert` listener was notified pre-2.16, the UoW was in a state where 1) all insertions for a particular class had been written to the database already and 2) the UoW had not yet gathered the next round of entities to process.
@mpdude
Copy link
Contributor Author

mpdude commented Oct 10, 2023

Discussed with @derrabus, @morozov , @beberlei and @greg0ire in-person that we do not want to deprecate flush() re-entrance in 2.x and prevent it in 3.0, since we consider it a legetimate use case to update or create new entities from event listeners, and for users it would feel natural to call flush() after that.

This of course means we need to spend some work on it:

  • Add tests for this behavior
  • Work out how event dispatching should work, e. g. will we call preFlush or similar events again?

One idea by @morozov was to have one class or data structure keeping track of what needs to be inserted/updated/deleted and in what order, and to make sure these orders remain valid when additional inserts/updates/deletes are added at a later time. A second class would in a way "traverse" or "work" through this structure until all operations have been performed.

That way, it might be possible to ensure that all flush() operations, including the re-entrant ones, happen in the same transaction.

If all that turns out to be not feasible, we can still prevent re-entrance in 4.0.

@mpdude
Copy link
Contributor Author

mpdude commented Oct 10, 2024

@alcaeus told me at the current hackathon that the ODM chose to prevent reentrance and thus flush() cannot be called there from event handlers that during the commit phase.

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.

3 participants