-
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
[10.x] Dispatch events based on a DB transaction result #48705
[10.x] Dispatch events based on a DB transaction result #48705
Conversation
It may be advised from fntneves/laravel-transactional-events's author @fntneves |
Thanks for bringing this up, I'll also closely monitor this PR. fntneves/laravel-transactional-events has been an integral part for years for the projects I'm working on and it's a great to not have to think about Also important to know is that laravel-transactional-events supports nested transaction very well. The bar is high 😁 |
Hi @mateusjatenee, It is good to see you bringing this concern into the core of Laravel! I've thought about it in depth in the past, let me know if you need any help improving it. |
@mfn the bar is most definitely very high! I actually though of reaching out to @fntneves first but I wanted to see if this would get any sort of traction first. He's done fantastic work with the package, and I would love to see this feature baked into the framework. The package's implementation is a bit more complex (and I think we could go that way in the future) — maybe even have the Dispatcher handoff the event to a |
Although it's not a BC, some people may prefer targeting for 11.x. It depends on maintainers' opinions |
@mateusjatenee what happens if no transaction is currently happening, or even doesn't happen during the entire request? |
If there is no transaction happening at the time the event is published, the callback is executed immediately (normal behavior). That behavior is guaranteed by We could also verify whether a transaction is happening on that if block, and skip if there's no active transaction. I also added a test that covers nested transactions (if the parent one succeeds and a child transaction fails, an event published on the parent one should still succeed as well). |
I expanded the scope of this PR a bit 😅 I didn't like how after this PR we would have two ways of indicating "afterCommit" behavior - most existing support for this feature utilizes a My updates continue to support the property based syntax but also introduce an interface based syntax to the other places in the framework that currently support "after commit" behavior (jobs, event listeners, mail, notifications, and model broadcasting). This is accomplished using a new class ExampleEvent implements ShouldDispatchAfterCommit
{
// ...
}
class ExampleListener implements ShouldHandleEventsAfterCommit
{
// ...
}
class ExampleJob implements ShouldQueueAfterCommit
{
// ...
}
class SomeMailable extends Mailable implements ShouldQueueAfterCommit
{
// ...
}
// etc... |
@fntneves would appreciate your final review on this PR and any thoughts you have since you have maintained a package to achieve a similar goal. ❤️ |
use Mockery as m; | ||
use Orchestra\Testbench\TestCase; | ||
|
||
class ShouldDispatchAfterCommitEventTest extends TestCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One test case that comes to my mind:
- Dispatch all events, including ones registered within nested transactions, only after the root transaction commits.
Is this already handled somehow by $afterCommit
implementation? It is important to do a sanity check, here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good catch. I'm adding some tests.
If you check DatabaseTransactionsManager
. It only executes the callbacks once the all the transactions have been handled. This is the ManagesTransactions::commit()
code:
public function commit()
{
if ($this->transactionLevel() == 1) {
$this->fireConnectionEvent('committing');
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
if ($this->afterCommitCallbacksShouldBeExecuted()) {
$this->transactionsManager?->commit($this->getName());
}
$this->fireConnectionEvent('committed');
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's it! Thank you, you're nailing it!
@fntneves @taylorotwell I've added two nested transactions tests that are almost the same — the difference is that in one, the parent transaction event is dispatched in the beginning, and on the second it, it's dispatched at the end.
DB::transaction(function () { // this adds a transaction at level 1
DB::transaction(function () { // this adds a transaction at level 2
Event::dispatch(new ShouldDispatchAfterCommitTestEvent); // this pushes a callback to the level 2 object
});
// This also pushes a callback to the level 2 object, because the level 2 object is only removed if the level 2 transaction is rolled back.
Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent);
}); As you can see, both callbacks get added to transaction 2 because it's the last object in the array. DB::transaction(function () { // Adds transaction at level 1
Event::dispatch(new AnotherShouldDispatchAfterCommitTestEvent); // Adds a callback to the level 1 object
DB::transaction(function () { // Adds transaction at level 2
Event::dispatch(new ShouldDispatchAfterCommitTestEvent); // Adds a callback to the level 2 object
});
// Although the child transaction has been concluded, the parent transaction has not.
// The event dispatched on the child transaction should not have been dispatched.
$this->assertFalse(ShouldDispatchAfterCommitTestEvent::$ran);
$this->assertFalse(AnotherShouldDispatchAfterCommitTestEvent::$ran);
}); I don't think this could cause any bugs right now because even if you have 2 I'm not sure I worded this properly, let me know if that makes sense. |
})->setTransactionManagerResolver(function () use ($app) { | ||
return $app->bound('db.transactions') | ||
? $app->make('db.transactions') | ||
: null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this make Illuminate Events depends on Illuminate Database package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the current implementation, if there is no instance bound (in this case, TransactionManager), the events are immediately dispatched (the afterCommit logic is the responsibility of the projects using it).
Therefore, it does not depend on the Illuminate DB package.
I'm following you. And I agree that that behaviour should not cause bugs. At least, in your example. What happens, though, if we have the following?
In this case, at the end of the root transaction, I expect the following callbacks to be triggered: #1 and #2. My doubts are about which level will Callback #3 be attached to and what happens to that level's object when the second nested transaction fails. If it is the same as the first nested transaction (level 2), I suspect that Callback #1 might never be called. While we can consider this might not happen, one reason I believe we should check it is when third-party packages already have transactions that combined with the ones in the main project, might cause such behaviour without one notice. This would be a nightmare to debug. |
@fntneves we are going to work on that as a separate bug / issue |
Since this has been merged, adding documentation to laravel/docs should be done. |
Working on docs here: https://github.com/laravel/docs/pull/9106/files |
Hey @mateusjatenee. This PR was merged with failing tests. Please only mark a PR as ready when it has passing tests. |
My bad @driesvints. I'll pay more attention next time — added a PR here #48858 |
This is another stab at #48631.
Right now, if we have event dispatching inside a database transaction, there are 2 ways to ensure events are not published:
#48631 aimed to provide syntatic sugar for that use case.
afterCommit
.I don't think option 2 is very good for a couple reasons:
Event::assertNotDispatched()
would fail.afterCommit
What this PR aims to do is to make the event itself aware of transactions. So, if a transaction fails, the event doesn't even get published.
That way, it doesn't matter if the listeners are queued or not or if they have
afterCommit
enabled, and you can ensure, in the tests, that the event did not get published.