-
Notifications
You must be signed in to change notification settings - Fork 11.1k
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
[8.x] Delay pushing jobs to queue until database transactions are committed #35266
Conversation
This touches the problem of "transactional events", very often neglected in frameworks => props! However, this should be optional and may not be desired by default. There might be job (e.g. for logging something?) which are independent of whether the transaction will be rolled back or not. There are other approaches like using a dedicated marker interface for opt-in. Please see mfn#1 for an attempt though I lacked the time to move forward but technically it's a working PR. Your input would be highly appreciated. See also:
|
Those jobs will still run even after a roll back. Unless an unhandled exception was thrown. |
Interesting approach. Rails has been lingering on solving this issue for quite some time and I think they've decided to go with 3 options (no PR yet though, only a discussion):
TBH the rollback job dropping seems wierd though and this PR seems to be a better approach imo. I do think that we should have an option to forcefully enqueue the job. |
I think giving the people the option to opt in is a good idea. Could be a config value inside queue.php. Will wait for @taylorotwell feedback first. |
Ah, thank you => important piece I missed and it's unrelated to what I suggested => apologies. |
Just for clarification: Why is it even necessary in @themsaid 's initial example in this conversation to put the job dispatching in the transaction? Isn't it enough to put it AFTER the transaction...? In that case the job will only be dispatched if the transaction succeeded, in case of rollback that transaction would throw an exception and no job would be dispatched Or probably i'm missing the obvious? |
@graemlourens the issue is that it's easy to forget that the job needs to be dispatched after the transaction. That's what this PR intends to solve. |
What @paras-malhotra said, but also your business logic might be agnostic and not know, and should not care, whether a transaction surrounds it or not. That's why basically years ago I started to use https://github.com/fntneves/laravel-transactional-events in my code. Only dispatch job after the transaction commits successfully and (in most cases) discard jobs if the transaction fails: you don't want to send an email invitation if something breaks later in the transaction cycle and reverts it. |
Thank you both for the clarification. Makes sense and the added benefit of protecting developers is a large one that i absolutely did not initially see. |
@paras-malhotra do you have a link to the related Rails discussion? |
@taylorotwell here they are: rails/rails#26045 and rails/rails#26103 |
So it sounds like we need a good summary of the types of behavior we want to support and probably the various things will need to be specified at the transaction level because you may want to different behavior in different parts of your code. Regarding a default of throwing exceptions anytime a job is enqueued within a transaction. I'm undecided on this. This is a pretty big breaking change and I'm not sure I have the appetite to take on a breaking change like that right now. I can definitely see the benefit of discarding jobs on transaction rollback or errors but again you would maybe want to be able to specify that the job should be queued anyways? Probably want a way to force a job to queue immediately even within a transaction? |
Please take a moment to look at mfn#1 , the gist is:
Note: I'm not pushing my PR, otherwise I would have submitted it already. I see the value of making this "job dispatch should be transaction aware" without having a dedicate marker interface, aka:
But I firmly believe the "transaction awareness" needs to a conscious decision by the developer. Note: this also means theoretically |
I'd suggest:
Regardless of whether we throw an exception or not, this will always be a breaking change unless we have |
Just want to share my pain related to this issue and transactional events: Anyone have similar issues ? Any approach recommended ? |
One more thing to mention: common pitfail to raise event that broadcasted to client via websocket inside DB transaction.
|
agree with @alsma I use events a lot. Queued listeners are doing their job and eventually dispatch another jobs or fire events So what about that case? DB::transaction(function(){
// (some logic here)
// this event should be launched regardless of the end result
// and listener for that will do some calculations and dispatches job that will analyze something
event(new SomeAttemptEvent);
// more logic here
$someModel = Model::create([]); // maybe fires another (queued) event on `creating`/`created` event
// more logic here
// should be fired only if everything was ok
// maybe listener for that dispatches another job
// this line won't be reached if something failed before
// no need to care about that
event(new SomethingFinishedSuccessfully);
}); It will be a little bit confusing which of these events and jobs should be fired and which should be not. |
@johnylemon events are always handled sync, aren't they? So I guess in your case, the Listener would actually by a mediator which uses the database-transaction-aware dispatch version and dispatch the actual job. OTOH https://github.com/fntneves/laravel-transactional-events does support "transaction events" |
@mfn nope, events are not handled synchronously everytime |
IDK, using a config option feels very wrong to me; but maybe that's just me. |
Guys I did some small research and want to share my thoughts on this. API can be something like following: DB::transaction(function() {
$user = User::register();
// ... some logic goes here
event(new UserRegistering($user)); // event listeners will be executed in scope of current DB transaction
DB::onceCommitted(function () {
event(new UserRegistered($user)); // event listeners will be executed after DB transaction commit
dispatch(new FinishRegistration($user)); // job will be dispatched after DB transaction commit
});
}); |
@alsma I have that in mind actually but want to test it with queued jobs first since it's the most common cause of problems. Once we have something stable, we can apply the same concepts to a generic function/interface that can be used with any code. So I think it's better if we keep the conversation in this PR only around queued jobs. |
When running the code above, the
SendWelcomeEmail
job may get dispatched and picked by a worker before the transaction is committed. This will lead to errors since the user model won't exist when the job runs.This PR is an attempt to capture queue dispatches, store them in a local cache, and only perform the dispatch when all transactions has been committed. Given the example above, the
SendWelcomeEmail
won't get dispatched to the queue until the transaction is committed.To enable this behaviour, you need to set the
after_commits
configuration value totrue
in the connection settings inside thequeue.php
config file. You can also use theafterTransactions
method when dispatching the job:In the case of rollbacks, the jobs will still get pushed to the queue. But in that case the errors that will happen when the job runs will make sense. This PR's only purpose is to delay dispatching until the transactions are committed or rolledback.
You can add a
dispatchAfterTransactions
public property to mailables, notifications, listeners, and broadcastable events to achieve the same behaviour.