-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Conversation
UnitOfWork::commit()
reentranceUnitOfWork::commit()
reentrance
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.
e281238
to
8b0c5ac
Compare
@beberlei maybe you recall if this was ever meant to work? |
Tests please. 🙂 |
☝🏻 – sure, as soon as we agree we want to go down that road. Converting PR to draft for the time being. |
UnitOfWork::commit()
reentranceUnitOfWork::commit()
reentrance?
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 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
} 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! |
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.
Discussed with @derrabus, @morozov , @beberlei and @greg0ire in-person that we do not want to deprecate This of course means we need to spend some work on it:
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 If all that turns out to be not feasible, we can still prevent re-entrance in 4.0. |
@alcaeus told me at the current hackathon that the ODM chose to prevent reentrance and thus |
This PR prevents reentrant calls to
UnitOfWork::commit()
with a clear exception message.Reentrance happens, for example, when users call
EntityManager::flush()
from withinpostPersist
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:
flush()
reentrance – otherwise, such a test would have failed with the changes suggested here.preFlush
orpostFlush
explicitly mentions thatflush()
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 callflush()
duringpostPersist
, in the middle of a transaction, when there are reasons against calling it inpostFlush
, just before final cleanups are made?UnitOfWork::$entityChangeSets
orUnitOfWork::$originalEntityData
. It's all to easy to mess up these states when we re-compute changesets mid-way through aflush()
– and again, if that were anticipated, I'd assume to find any kind of test coverage.TODO
EntityManager::flush()
reentrance since 2.16.0 #10906 – revert that change, maybe in 3.0?