Replies: 7 comments 27 replies
-
Interesting idea. But that would only work for actions on the AR that can happen async. If you have a simple action that modifies the AR during the lifecycle of the request so you can return the result immediately this won't solve the issue of the second request failing due to concurrency. |
Beta Was this translation helpful? Give feedback.
-
I little visualisation of what the command flow would look like command-bus.mov |
Beta Was this translation helpful? Give feedback.
-
What if my application needs to execute the commands in a synchronous way? |
Beta Was this translation helpful? Give feedback.
-
What if my command triggers multiple ARs, for example:
Even though MyCommand is handled in sequence, ReactorA is async, you can not guarantee its order again, now you are back to square one. |
Beta Was this translation helpful? Give feedback.
-
What about the WithoutOverlapping middleware? (https://laravel.com/docs/8.x/queues#preventing-job-overlaps) I'm on Laravel Vapor and thought this might be a solution for me. (Queues on Vapor are NOT FIFO) Example: class OfferLoanReactor extends Reactor implements ShouldQueue
{
public function middleware()
{
return [(new WithoutOverlapping('NO IDEA HOW TO GET $event->aggregateRootUuid() HERE'))->expireAfter(180)];
}
public function __invoke(MoreMoneyNeeded $event)
{
$account = Account::where('uuid', $event->aggregateRootUuid())->first();
Mail::to($account->user)->send(new LoanProposalMail());
}
} Maybe a generic Also please keep in mind Horizon is not available on Vapor. It would really be awesome if Vapor would be supported. |
Beta Was this translation helpful? Give feedback.
-
I think https://github.com/temporalio/sdk-php is a great solution for this. My reactors just send signals into currently running temporal workflows. |
Beta Was this translation helpful? Give feedback.
-
I recently opened a draft PR to improve on how we handle concurrency: #424 |
Beta Was this translation helpful? Give feedback.
-
After fixing #170, we need to think about better ways of handling concurrency. Here's the problem briefly described:
#170 fixes an issue where two instances of the same aggregate root could persist different events at the same time. Imagine two HTTP requests coming in, both triggering an event on the same AR instance (that is, one with the same UUID). If both events are recorded simultaneously, it could result in a corrupted state, because they didn't know of each other.
The solution to prevent such invalid state is the ensure there can always only be one event per AR uuid and AR version on the database level, which is how #170 was fixed. While invalid state is prevented, there's still an error for the developer to deal with. That's what we want to improve in v5.
The cleanest solution is to use commands (a basic command bus is already added in v5). We could schedule commands on a queue that's only able to process one command at a time, and forces the aggregate root to be retrieved right before execution. That way we're sure always the latest version of any given AR is used.
While this solution works, it's not the most performant: if every concurrent command in the system needs to be processed by a single queue, there's a significant potential for bottlenecks.
Instead, if we'd have a mechanism in place that only allows one command to run per AR instance, we're safe as well. We could run x-amount of commands in parallel, but only have one command per AR at any given time.
This begs the question though: is it possible to achieve this behaviour in Horizon, or do we need to write our own scheduler? I want to avoid rewriting parts of Horizon if possible, so I'm looking for feedback before diving into this.
Edit: we've decided to only support basic command bus middlewares for now, so
app(CommandBus::class)->middleware(new RetryMiddleware());
is possible, but we won't add a custom horizon queuing mechanism for now. We might add it later, but probably not in v5Beta Was this translation helpful? Give feedback.
All reactions