A simple explanation about concurrency with Laravel Octane
Marco Oliveira
Posted on October 5, 2023
What to expect
Don't feel bad if you never heard about concurrency. This word became popular to PHP developers only after Swoole (An event-driven, asynchronous, coroutine-based concurrency library with high performance for PHP. - by Swoole's readme).
In this quick and direct article, I will show you how to use Laravel Octane (A Laravel Package to boost performance using servers like Swoole and OpenSwole) to implement concurrency.
In the end, I hope you understand the basics of how concurrency works, what it is Swoole and Octane, and how to use concurrency with Octane.
Requirements/setup
So, the basic requirements to follow properly this article will be:
Install Laravel with Sail (https://laravel.com/docs/10.x/installation#laravel-and-docker);
Follow this documentation to install properly Swoole and Octane in Sail (https://laravel.com/docs/10.x/octane#swoole-via-laravel-sail)
A small change to
supervisord.conf
. Add this option to the command--task-workers=5
, so it will be something like this:
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --watch --server=swoole --host=0.0.0.0 --port=80 --task-workers=5
Don't bother to understand everything in this line right now. But for explanation purposes:
--watch
: set the watching process for changes on, so you don't have to restart your octane server for every single change;
--server=swoole
: use the swoole as the server (it could be openswoole or roadrunner - but roadrunner don't support concurrency)
--task-workers
: I'm limiting the number of task workers (workers available for the concurrency execution, We will come back to it in a few minutes)
Our problem
Now we are ready for the real fun.
Let's put this scenario, you have these huge queries in a single request. If you have worked in a large application you probably faced this scenario before: slow queries in a single request to compose different information for that part of the application.
Let's say you have 5 of these huge queries (or any slow code execution, I'll stitch with queries now just for the purpose of simple explanation), each takes 1s (yes, they are that bad) so only waiting for the queries to be solved you spent 5 precious seconds of your users. In this scenario, concurrency will be a game change.
Laravel Octane::concurrently
I was amazed by this the first time I read the documentation for it. IMHO this can really boost Laravel application to a whole new level. Enough of opinions, let's see how to use it.
The concurrently
method of the Octane facade expects a list (array) of tasks, each will be executed as callbacks so you can provide anonymous functions. Remember our example from before? To keep it simple, let's say that we need to query 5 different tables:
[$users, $bills, $orders, $entries, $visits] = Octane::concurrently([
fn() => User::someAmazingFilters(),
fn() => Bill::someAmazingFilters(),
fn() => Order::someAmazingFilters(),
fn() => Entry::someAmazingFilters(),
fn() => Visit::someAmazingFilters()
]);
If you are not used to arrow functions, the same in conventional function declaration would be:
[$users, $bills, $orders, $entries, $visits] = Octane::concurrently([
function () {
return User::someAmazingFilters();
},
...
]);
But to keep it short we are going to use the arrow functions declarations in this article.
Don't bother about the entities' names. I just wanted to show you that the response is also an array of returns from each anonymous function, so you use the destructuring syntax to keep it pretty.
Now let's dive deep to understand what is happening here. Remember about the 1s of waiting for each query? I want you to track it with me, so we are going to change the above code just a little:
Octane::concurrently([
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
]);
So, if we are executing the same code, without the concurrently
method we would have a 5s sleeping time, right? But if we use the code above we only wait 1s, I hope you bear with me in this other example:
Octane::concurrently([
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
fn() => sleep(1),
]);
Instead of 5 sleep tasks we have 6, what do you think is going to happen in this scenario? Keep in mind that we limit the task workers to 5.
If you said that the total time will be 2s you are correct. And why is that?
In the first scenario, we have 5 tasks and 5 task workers:
So it executes 5 concurrently:
The worst execution time is 1s, so it takes 1s.
But in the second scenario, we still have 1 task holding the execution waiting for one of the tasks in the task workers to be solved properly:
Just one more thing to understand. Let's imagine that we have 5 tasks with this list of execution times (1,2,1,1,1). What would be the total time of execution of this scenario? You are right if you said 2 seconds.
One last thing to keep in mind, the prioritization of tasks is based on the array position, so a list with (1,2,1,1,1,1) would take ~2 seconds, but a list with (1,1,1,1,1,2) would take ~3 seconds. Keep in mind that it depends on other factors, but to understand that order matters, let's keep it as it is.
I hope you enjoy this simple explanation of how to use concurrency with Octane, but I have to alert you there is so much more about concurrency, Swoole, and Octane than what we saw in this article.
If you like it I would recommend some links to learn more about it:
https://laravel.com/docs/10.x/octane#swoole - for a full list of features available on Octane.
https://openswoole.com/docs/modules/swoole-server-taskWaitMulti - for full documentation on the method taskWaitMulti
which is the method from Swoole and OpenSwoole that Laravel Octanes uses on the concurrently
method.
If I helped you or if I missed something here, please let me know in the comments.
Posted on October 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.