Getting to know the application server for Laravel Octane
In the PHP ecosystem, we have several application servers.
Laravel Octane, which handles server configuration, startup, and execution, integrates mainly with two of them: Swoole and RoadRunner.
We will deal with the installation, configuration, and use of these two application servers in detail later on.
For now, it is enough for us to know that once the application servers are installed, Laravel Octane will take care of their management. Laravel Octane will also take care of their proper startup via the following command:
php artisan octane:start
The octane:serve
command is added when Laravel Octane is installed.
In other words, Laravel Octane has a strong dependency on application servers such as RoadRunner or Swoole.
At startup, Laravel Octane via Swoole or RoadRunner activates some workers, as shown in the following figure:
Figure 1.1: The activation of workers
What are workers?
In Octane, a worker is a process that takes charge of handling the requests associated with it. A worker has the responsibility of starting the framework and initializing framework objects.
This has an extremely positive impact from a performance standpoint. The framework is instantiated on the first request assigned to the worker. The second (and subsequent) requests assigned to that worker reuse the objects already instantiated. The side effect of this is that the worker shares instances of global objects and static variables between requests. This means that different calls to the controller can access the data structures that are shared between requests.
To complicate matters, there is the fact that requests assigned to the same worker share a global state, but different workers are independent and have scope independent of each other. So, we can say that not all requests share the same global state. Requests share a global state when associated with the same worker. Two requests from two different workers share nothing.
In order to minimize the side effect, Laravel Octane has the responsibility of managing the reset of classes/objects owned directly by the framework across the requests.
However, Octane can’t manage and reset classes owned directly by the application.
That’s why the main thing to pay attention to when using Octane is the scope and lifecycle of variables and objects.
To understand this better, I will give you a very basic example.
Example with a shared variable
This example, in the routes/web.php
file, creates a route for path /
and returns a human-readable timestamp. To simplify the explanation, we are going to write the logic directly into the route file instead of calling and delegating the logic to a controller:
$myStartTime = microtime(true); Route::get('/', function () use($myStartTime) { return DateTime::createFromFormat('U.u', $myStartTime) ->format("r (u)"); });
In the routes/web.php
routing file (web.php
is already stored in the routes
directory in the Laravel root folder project), a $myStartTime
variable is instantiated and assigned the current time expressed in milliseconds. This variable is then inherited by the route/management
function via the use
clause.
In the performance of the function associated with route/
, the contents of the $myStartTime
variable are returned and then displayed.
With the classic behavior of the Laravel application, at each invocation/execution, the variable is regenerated and reinitialized (each time with a new value).
To start the Laravel application in the classic mode, simply run the following:
php artisan serve
Once the web server is started, go to the following URL via the browser: http://127.0.0.1:8000
By continuously reloading the page, a different value is displayed each time; basically, the timestamp is displayed with each request.
Instead of using the development web server provided by Laravel, you would use Laravel Octane and have a different result. At each page refresh (reloading of the web page), you would always see the same value. The value is relative to the timestamp of the first request served. This means that the variable is initialized with the first request and then the value is reused across the requests.
If you try to refresh multiple times, in some cases, you could see a new value.
If this happens, it means that the request was managed by the second (or a new) worker. This means that this behavior is quite unpredictable because Octane acts as a load balancer. When a request comes from the network, the application server will decide which worker (of those available) to assign the request to.
In addition to this, another element that could cause a new value to be generated is when you hit the maximum number of requests managed by a single worker. We will see how to define the maximum number of requests later, and in general, we will have a deep dive session (in Chapters 2 and 3) into Laravel Octane configuration.
The behavior whereby variables are shared across workers until the application server is restarted is valid only for global variables or objects stored in the application service container. The local variables (the variables for which the scope is limited to a function or a method) are not affected.
For example, in the code previously shown, I’m going to declare a $myLocalStartTime
variable in the function called by the routing mechanism. The scope of the $myLocalStartTime
variable and its lifecycle is limited to the Closure
function:
$myStartTime = microtime(true); Route::get('/', function () use($myStartTime) { $myLocalStartTime = microtime(true); return DateTime::createFromFormat('U.u', $myStartTime) ->format("r (u)") . " - " . DateTime::createFromFormat('U.u', $myLocalStartTime) ->format("r (u)"); });
Execute the following command with the classic Laravel web server:
php artisan serve
You will see that both values will change on each new request. You can see that when you open a browser to http://127.0.0.1:8000
.
Launch Octane as a server with the following command:
php artisan octane:start
You will see, in your browser at http://127.0.0.1:8000
, two different dates/times with milliseconds. If you refresh the page, you will see a change in just the second one ($myLocalStartTime
).
You have to be aware of this behavior when you are building an application based on Octane.
Another example to better understand this behavior is creating a class with a static property.
Creating a class with a static property
In order to keep this example as simple as possible, I created a MyClass
class in the routes/web.php
file.
I’m going to add new routes that call the add()
method of the MyClass
object and then call and return the value of the static property retrieved by the get()
method.
In routes/web.php
, add the following class:
class MyClass { public static $number = 0; public function __construct() { print "Construct\n"; } public function __destruct() { print "Deconstruct\n"; } public function add() { self::$number++; } public function get() { return self::$number; } }
Then, in the routes/web.php
file, declare the new route as follows:
Route::get('/static-class', function (MyClass $myclass) { $myclass->add(); return $myclass->get(); });
Next, you can launch Laravel in a classic way using the following command:
php artisan serve
Now, if you access the URL http://127.0.0.1:8000/static-class
multiple times, the value 1
will be shown. This is because, classically, for every request, the MyClass
object is instanced from scratch.
Launch Laravel Octane using the following command:
php artisan octane:serve
If you then access the URL http://127.0.0.1:8000/static-class
multiple times, you will see the value 1
in the first request, 2
in the second, 3
in the third, and so on. This is because, with Octane, MyClass
is instanced for every request, but the static values are kept in memory.
With a non-static property, we can see the difference as follows:
class MyClass { public static $numberStatic = 0; public $number = 0; public function __construct() { print "Construct\n"; } public function __destruct() { print "Deconstruct\n"; } public function add() { self::$numberStatic++; $this->number++; } public function get() { return self::$numberStatic . " - " . $this->number; } }
After calling the page five times, the result shown in the browser will be as follows:
Construct Deconstruct 5 – 1
This is quite simple but, in the end, good for understanding the behavior of static variables under the hood.
The use of static variables is not so unusual. Just think of singleton objects or the app container of Laravel.
To avoid unexpected behavior – as in this specific example with static variables but more generally, with global objects (Laravel makes extensive use of them) – explicit re-initialization must be taken care of. In this case, the static variable is initialized in the constructor. My suggestion is to use explicit initialization of the properties in the constructor. This is because it is the developer’s responsibility to take care of the re-initialization of the variables in the case of global states (objects and variables).
class MyClass { public static $numberStatic = 0; public $number = 0; public function __construct() { print "Construct\n"; self::$numberStatic = 0; } public function __destruct() { print "Deconstruct\n"; } public function add() { self::$numberStatic++; $this->number++; } public function get() { return self::$numberStatic . " - " . $this->number; } }
We have seen just some very basic examples of the impact on the code if you are going to install and use Laravel Octane. The examples shown earlier were purposely very simple, but with the goal of being easy to understand. In the chapter where we will use Octane in a real scenario, we will cover more realistic examples.
Now we will analyze the impact on performance. So, by installing Octane, what kind of improvement could we have in terms of performance?