mirror of
https://github.com/doctrine/orm.git
synced 2026-03-23 22:42:18 +01:00
Emit postFlush event after transaction commit. #5906
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @d-ph on GitHub (Mar 1, 2018).
Hello,
Would it be possible to emit postFlush event (or just call
EntityManager::flush(), which would do it by itself) just after db transaction commit inEntityManager::transactional(), please?In other words, something like the following:
The use case is this: I have a postFlush listener that:
Connection::isTransactionActive(). During the first flush intransactional(), my listener wouldn't do its job because the flush happened in a db transaction.I currently work-around the current lack of the second flush by manually flushing after
transactional(). I don't, however, like how I (the end user) should be concerned with this vendor implementation detail. I'd much more appreciate this detail stay in the vendor code (i.e. Doctrine's)Thank you,
@d-ph
@Ocramius commented on GitHub (Mar 1, 2018):
Something has gone horribly wrong: executing a
flush()in a transaction or outside of it shouldn't make any difference.@d-ph commented on GitHub (Mar 1, 2018):
Hi,
Thanks for a light-speed response.
Are you sure it shouldn't make any difference? The result of the flush before the commit is visible only to the current db transaction / current pdo connection. It's not visible to another OS process (which has its own db connection open). Only after the
Connection::commit(), everything that happened in the transaction becomes visible to the "outside world" (assuming the commit succeeds, which is another topic, but irrelevant to this discussion)@Ocramius commented on GitHub (Mar 1, 2018):
Depends on the transaction isolation level that you are running.
SERIALIZEDtransactions aren't visible to other transactions (by design), so if you do want to read that data (at the risk of having ghost reads), you may configure your transaction isolation level differently.@d-ph commented on GitHub (Mar 1, 2018):
Ok, let me think about it and test different serialisation levels. Maybe I'm missing something here. I got to get going now, so I'll respond in 24h.
@d-ph commented on GitHub (Mar 1, 2018):
Hi again,
So I refreshed my knowledge about db transaction isolation levels and also ran some tests on my own to see when new records are actually visible in the database, and came to conclusion that everything I've said so far is correct.
Transaction isolation levels are for what data the queries inside the transaction see, not the other way around. I.e. it doesn't matter what the transaction isolation level is -- what happens in a transaction, stays in the transaction until it's committed. And by the way, I was testing with the default, least restrictive transaction isolation level in postgres: "read committed". The bottom line here is: other db connections from other OS processes won't see the data my script is putting in the db until my script commits the transaction. Which again, happens after the first flush, as opposed to: during the first flush, which makes my postFlush listener ignore it and wait further for "the final flush".
You said that
flush()ing inside or outside of db transaction shouldn't make any difference. It does. Here's a quick rundown of what happens, whenEntityManager::transactional()is used:1.2.1. $conn->beginTransaction();. At this point
private $_transactionNestingLevelis 2.2.2. $conn->commit();. At this point
private $_transactionNestingLevelis set back to 1. Nothing is visible to the outside world yet.private $_transactionNestingLevelis set back to 0 and a SQLCOMMIT;is run, which makes everything that happened in the transaction visible to the outside world.flush(), postFlush listeners that do stuff on "the final flush" won't have the opportunity to do their job.Bear in mind that all I care about is the postFlush event, not the second flush per se. But since running a second (noop) flush triggers the postFlush event, I'm ok with that solution.
@Ocramius commented on GitHub (Mar 2, 2018):
I don't really understand what you are trying to achieve here: all data
within the current and previous nested flush-related transaction is
readable after or at the end of a flush operation, so unless your listeners
are running on a separate process, you shouldn't need any tweaking at all.
On 1 Mar 2018 22:43, "d-ph" notifications@github.com wrote:
@d-ph commented on GitHub (Mar 2, 2018):
My postFlush listener kicks off another OS process via a queue solution (rabbitmq, but that doesn't matter). That another OS process absolutely must be able to read what my php script (with the postFlush listener) is flushing to the database. This is not the case after the first
flush()because thatflush()happens before a transaction commit and therefore the data is not yet visible to the outside world.I understand that my postFlush listener can read the data after the first or the second or even before any of the flushes. But that doesn't matter because my postFlush listener doesn't read any data -- it just kicks off another OS process (probably on a different machine as well, but that's irrelevant).
One could argue: what's the problem? The commit happens immediately after the first flush, so that should be fast enough. It turns out it's not. During those couple of milliseconds between the first flush and the db commit, the other OS process manages to start and fail a couple of times due to unreachable db data.
That's why the other OS process must start only after the transaction is committed. And the other OS process is "started" by my postFlush listener, so my postFlush listener should run after the transaction is committed. And that's what's not happening right now.
If running the second flush or if explicitly triggering the postFlush event is not desirable after the transaction commit, then maybe a new event should be introduced. E.g. postCommit. I'd personally rather avoid adding new events in this case, since at least to me triggering the postFlush again after the db commit makes perfect sense.
All boils down to the fact that I need a way to know when all in-memory content is flushed to the database, so other OS processes can access it. So far I've been using the postFlush event for it. But this falls short in the case of
EntityManager::transactional(), which doesn't communicate when the in-memory content is completely flushed to the database.@Isinlor commented on GitHub (Apr 6, 2018):
I think you could try to collect the relevant events and trigger actual dispatching after your application has finished all processing and you are sure that all transactions are closed. In Symfony it would be HttpKernel::TERMINATE event.
@d-ph commented on GitHub (Apr 7, 2018):
Thanks for your reply.
I already have a makeshift solution to my problem (calling the
EntityManager::flush()again after thetransactional()is finished), which I'm happy to continue using for time being. I also have bad experience with doing things on kernel terminate event (essentially, error handling is not ideal according to my taste).This ticket is getting old. If after all my explanation there's still no intention to implement what I asked for, then I'm happy to close this ticket. I do still, however, believe that my feature request is legitimate.
@errogaht commented on GitHub (Nov 1, 2018):
@d-ph your feature request is the same what I need too. Exactly the same problem with message bus and not closed transaction. I also vote for afterCommit event
@Isinlor commented on GitHub (Nov 1, 2018):
@errogaht Doctrine is not the right level of abstraction to dispatch
afterCommitevent. Doctrine does not actually know whether transaction was committed, since this is handled by DBAL and DBAL allows nesting transactions outside Doctrine.If you really, really want to hook into it then this line
$logger->stopQuery();gives you that opportunity.You can combine it with
PDO::inTransaction, to be 100% sure.Actually, just overwriting
PDO::commitand pushing that into DBAL should work too.@kgrieco commented on GitHub (Nov 6, 2018):
I'm also experiencing exactly the same problem :(
@jelen07 commented on GitHub (Jan 29, 2019):
Any progress here?
@Isinlor commented on GitHub (Jan 29, 2019):
@jelen07 Have you read my comment above? Have you tried it?
@ismail1432 commented on GitHub (Apr 29, 2019):
I have the same issue 👍 The
afterCommitevent seems a very good feature@gquemener commented on GitHub (Apr 15, 2020):
Allow me to share my personal experience while trying to perform the same kind of "after transaction commit" operations. I feel that it could help other people facing the same issue.
Don't forget the ConsoleEvents::TERMINATE event as well, if your process is running in CLI.
I'm having a similar issue, and thus would like to point out my use case.
I'm using the
postFlushevent to dispatch domain events which are recorded within my entity when some mutations are performed.The requirement being to ensure that the domain events are dispatched if and only if the entity has been persisted (i.e. db row has been updated and this data is accessible by others).
My understanding of the documentation was that the
postFlushevent would occur once the transaction is commited (if theflush()is called after a transaction has begun).It seems that this is not the case and
flushwill dispatchpostFlushevents even if a "master" transaction is still opened. Thus, this doctrine event doesn't meet the requirement I've previously stated, and it's not meant to do "after transaction commit" operations.Another way to do it is to delegate the domain events dispatching to another event occuring after the transaction is commited, like the
HttpKernel::TERMINATEandConsole::TERMINATEevents from Symfony. I don't see any drawback, yet, except that it would tightly couples your application with a framework (but this is also the case with a doctrine listener) and the error handling issue that @d-ph mentionned.Finally, another solution I find more elegant would be to have a Command and an Event bus. You wrap all command handling within a doctrine transaction and dispatch the domain events (in the event bus) once the command has been handled (and after the transaction is commited).
Like this you have total control about when and where "post commit" operations are performed and you don't rely on any library events.
FYI, Symfony Messenger component already provides a DoctrineTransactionMiddleware that does exactly that (wrapping message handling within a Doctrine transaction). When using Symfony Messenger, "performing extra stuff (or in this example, dispatching domain events) after transaction commit" would probably be implemented through another middleware configured on the command bus (decorating the doctrine middleware).
@mtanasiewicz commented on GitHub (Jul 29, 2020):
Hello. According to @gquemener solution, from what I've observed recently, closing transaction with messenger event bus, won't help in all cases. Most of lifecycle events are fired on every transaction layer that was added before. If we dispatch some domain events/commands when subscribing to lifecycle, it can end up dispatched multiple times, on each transaction layer.
There is one quite simple solution, I came up with after couple hours of battling. You can call
$entityManager->->getConnection()->getTransactionNestingLevel()to check if there are any remaining transactions to close. Returned value greater than1is most likely result with firing same lifecycle event again by Doctrine.@pcha commented on GitHub (Nov 6, 2020):
Hello. @mtanasiewicz where call you $entityManager->->getConnection()->getTransactionNestingLevel() to know if the commit was executed?
@xammmue commented on GitHub (Jul 5, 2022):
We encountered the same problem recently and were able to solve it using the DispatchAfterCurrentBusStamp / DispatchAfterCurrentBusMiddleware
https://symfony.com/doc/5.4/messenger/dispatch_after_current_bus.html
This won't help if your process is started by an HTTP request or command execution, but can solve the issue when handling messages and sending out other messages from within the handler