Isolating global variables with a coroutine context manager in Swoole

By Luke Embrey @ Swoole Labs

How to manage and isolate global PHP variables when using a Swoole server and coroutines.

Introduction

Compared to traditional PHP where you run servers behind an Apache or Nginx HTTP service with either mod_php or PHP-FPM enabled, your PHP applications would run using a stateless model whereas Swoole runs using a stateful model. By design Swoole runs in memory, it takes advantage of being able to save things in memory so you don't have to reload everything for every request coming into your servers.

What is the main difference between stateless and stateful? - With traditional PHP, if you are using Apache or PHP-FPM, then every request is separated away from every other request, there is no crossover between request processes, making it easier to manage variables and not leaking data into other HTTP requests. However, Swoole runs in memory and maintains a stateful process, this means that there is chance for memory leaks between requests, especially with PHP global variables like $_GET, $_POST, $_FILES and $_COOKIE etc. But, taking advantage of the stateful model opens up more benefits like speed, efficiency and a shared application design.

A stateful model where everything is kept in memory between requests poses a problem, you will run into memory leaks and leak data into another request, especially if you are using PHP global variables or the PHP Session System and have coroutines enabled, as coroutine switching means your Swoole server will switch between separate requests which are sharing the same worker process while another coroutine is waiting/blocking for I/O, like for a result to return from MySQL etc.

With all that being said, Swoole brings us a lot of new benefits and performance, it is worth learning how to design applications when using Swoole, let's explore how we can navigate around managing variables between requests.

What are coroutines?

To understand why we need to isolate variables between requests and coroutine contexts, it is important to understand why coroutines introduce this problem by understanding how they work first.

Swoole implements coroutines based on I/O switching and can be simply understood as a user-land thread. Swoole coroutines are cheap to execute and lightweight when switching between different coroutines. Within Swoole, coroutines are managed by the coroutine scheduler and can automatically switch context based on I/O operations, for example, when waiting for a database query to return, another coroutine can be executed so no time is wasted doing nothing. Coroutines in Swoole are very similar to how goroutines work in Go-lang and does not rely on future/promise APIs, callbacks or async/await API.

Check out the full coroutine documentation to read more.

Coroutines are very efficient because the cost of creating, destroying and switching is very low and within a coroutine context, they all run under the same process-space, so the CPU core which is being used is running at maximum efficiency because no time is wasted doing nothing, everything happens concurrently.

Why are global variables and PHP Sessions affected?

As we have said, Swoole runs using a stateful design model and keeps everything in memory, worker processes are started to handle requests and worker processes can handle multiple request at a time, if one request is waiting for I/O operations to complete the same worker will start processing a new request, no time is wasted. As this is all happening within the same process or user-space as the other request, any global variables or objects will share data, but that is how Swoole achieves maximum efficiency and concurrency for great performance compared to traditional PHP.

Isolating Variables in Stateless Applications (PHP-FPM, FastCGI etc.)

A lot of the time, developers are looking for a way to speed up their applications but they come from a PHP-FPM environment, where the project was developed in a stateless manner and Swoole would cause memory leaks if not updated. To setup an easy solution to this global variable problem, we can use the coroutine context to switch between and isolate global variables and access them with a slight difference.

The first thing to know is that in Swoole, each time a request takes place, Swoole creates a coroutine context (or container) for you, this coroutine context will have its own context ID.

<?php

// Enable all coroutine hooks before starting a server
Swoole\Runtime::enableCoroutine(SWOOLE_HOOK_ALL);

$server = new Swoole\HTTP\Server("127.0.0.1", 9501);

$server->set([
    'enable_coroutine' => true,
]);

// ...

// The request handler callback function
$server->on('Request', function($request, $response)
{
    // You do NOT need to do this, Swoole creates the context for you
    Co\run(function()
    {
        // Every new request will be inside a `Co\run` coroutine context/container
    });
});

// Rest of server setup code...

The example above shows us a snippet relating to running a Swoole HTTP Server, the Request event is called for each new HTTP request and if your server has coroutines enabled, the server will create the Co\run context for you, above was just an example so you can understand what is going on and how we will use a coroutine context to access and isolate global variables between different requests.

If we just used any global PHP variables within the Request event like $_GET, $_POST, $_FILES and $_COOKIE etc. Then we would eventually experience a memory leak because Swoole maintains process state and runs in memory across requests. That is why we need to use the context of coroutines to implement a Context Manager, allowing us to easily access global variables without any memory leaks and still benefit from the performance and concurrency of coroutines.

Coroutine Context Manager

We can implement a basic Context Manager, allowing us to access and use different coroutine context objects, preventing any memory leaks and allowing us to emulate PHP global variables.

<?php
class ContextManager
{
    // Set is used to save a new value under the context
    public static function set(string $key, mixed $value)
    {
        // Get the context object of the current coroutine
        $context = Co::getContext();

        // Long way of setting a new context value
        $context[$key] = $value;
        $content->key = $value;

        // Short method of setting a new context value, same as above code...
        Co::getContext()[$key] = $value;
    }

    // Navigate the coroutine tree and search for the requested key
    public static function get(string $key, mixed $default = null): mixed
    {
        // Get the current coroutine ID
        $cid = Co::getCid();

        do
        {
            /*
             * Get the context object using the current coroutine 
             * ID and check if our key exists, looping through the
             * coroutine tree if we are deep inside sub coroutines.
             */
            if(isset(Co::getContext($cid)[$key]))
            {
                return Co::getContext($cid)[$key];
            }

            // We may be inside a child coroutine, let's check the parent ID for a context
            $cid = Co::getPcid($cid);

        } while ($cid !== -1 && $cid !== false);

        // The requested context variable and value could not be found
        return $default ?? throw new InvalidArgumentException(
            "Could not find `{$key}` in current coroutine context."
            );
    }
}

// Our HTTP server instance
$server = new Swoole\HTTP\Server("127.0.0.1", 9501);

// ...

// The request event handler callback
$server->on('Request', function($request, $response) 
{
    /*
     * At the start of every new request, setup global 
     * request variables using Swoole server methods.
     */
   ContextManager::set('_GET', (array)$request->get);
   ContextManager::set('_POST', (array)$request->post);
   ContextManager::set('_FILES', (array)$request->files);
   ContextManager::set('_COOKIE', (array)$request->cookie);

  // And when you use them
  echo ContextManager::get('_GET')['foo'] ?? 'bar';

  // Instead of traditional PHP global variables...
  echo $_GET['foo'] ?? 'bar';
});

Our Context Manager example above is just a basic working class, more can be done to improve it but it shows the basic usage of how a coroutine context works and how we can emulate global PHP variables without introducing any memory leaks in our application.

This is all made possible by using Co::getContext() and Co::getCid(). When calling getContext, it will return a Swoole\Coroutine\Context object which is created by Swoole for every coroutine context/container, this object exists during the lifetime of the coroutine, Swoole will automatically handle any cleanup processes after the coroutine exits, allowing you share data without leaking to another request/context. By default getContext with no arguments will use the current coroutine ID but you may use getCid to get the current coroutine ID or another as well.

To explain in more detail why we have to use a do-while loop and account for being deep in sub coroutines, consider this example:

<?php
Co\run(function() 
{
    var_dump(Co::getPcid());

    go(function()
    {
        var_dump(Co::getPcid());

        go(function()
        {
            var_dump(Co::getPcid());

            go(function()
            {
                var_dump(Co::getPcid());
            });

            go(function()
            {
                var_dump(Co::getPcid());
            });

            go(function()
            {
                var_dump(Co::getPcid());
            });

        });

        var_dump(Co::getPcid());
    });

    var_dump(Co::getPcid());
});

The code example above shows what it may look like when inside a request which is using a lot of different coroutines and why we have to loop through all potential context objects but stop when we reach the Co\run parent context.

Finally, we can use the Context Manager to emulate how super global variables work in PHP, for example, when we set $request->get to _GET as the key within the Context Manager, we are using the Swoole provided $request object to store all the request data instead of $_GET. The same goes for all the other super globals shown above.

If you wish to further explore the Context Manager example, some things which could be added/improved:

  • Checking if the call is within a coroutine first
  • Implementing a has() method to see if a key exists
  • A destroy() method to remove a coroutine context
  • Accessing another context from the current coroutine

Coroutine Context Architecture

How does each coroutine context work? - The diagram below shows how each request will have its own coroutine context/container and how we can use our ContextManager to access the right global variables and not create a memory leak.

Swoole coroutine diagram
The architecture behind how coroutine context works

A worker process can handle multiple requests at once when coroutines are enabled, a worker may switch between another request when another coroutine is waiting for I/O operations to complete (like a return from MySQL). The diagram above shows how when we access the coroutine context, we are passed the right output from our GET string query, even if the worker is handling two requests, the ContextManager prevents any memory leaks. Remember that the Swoole server will automatically create the Co\run environment for you for each request.

Isolating Variables in Stateful Applications

Even though the context manager approach is one of the best ways to share data that is global without introducing any memory leaks, a context manager helps applications that were designed for a stateless system run under Swoole, but for applications which have been designed and built with Swoole in mind from the beginning, can take a more focused approach and benefit more from Swoole and its stateful design model.

It is recommended that you take advantage of a context manager so that you can manage global data without memory leaks between coroutines but in a stateful application designed for Swoole, you can benefit from other features such as running a global singleton service, having a custom request system by setting everything up inside the Request event before processing a new request and managing PHP object instances which run globally and which run per request.

The main point here is that you are more free when you develop with a stateful model in mind but Swoole can be used to support both stateful and stateless systems. It's just in a stateful application, you take take even more advantage of Swoole.

PHP Sessions and Coroutines

We have seen in this article how to emulate super global PHP variables like $_GET and $_POST etc. But what about traditional PHP Sessions (session_* functions), can you use them in Swoole? The short answer is no, PHP Sessions will leak data across requests and won't work under Swoole's stateful design, the super global $_SESSION will share memory across requests and cause integrity issues if used.

Instead, it is recommended that you store session data inside a database like MySQL or a memory system like Redis etc. then inside your $server->on('Request') event, setup/initialise your session before processing the request further, getting the session data from wherever you choose to store it. That way you can make it global using the Context Manager example or build a Session class to access your external session store. Then there is no need for any session abort or cleanup functions, saving time and resources.

Conclusion

Hopefully, you have learnt how to manage global variables compared to traditional PHP like when using PHP-FPM. In the examples here we can see how a Context Manager can be useful to emulate PHP global variables and implement a session system using a context variable, all while still being able to benefit from using coroutines and their high performance.

As the PHP world moves away from the stateless execution model and adopts a more scalable design like how NodeJS provides an asynchronous system for JavaScript developers, Swoole provides the same for PHP developers, even expanding on the asynchronous design and concurrency of event-driven servers. We have to understand how to adapt to different ways of working and this article is a great example of how a lot of new developers coming to Swoole are so used to using popular PHP features like super globals and $_SESSION, but as you can see it is easy and quick to integrate with the new powers of Swoole with just a few changes to consider.

Check out the full coroutine documentation to read more.

Send your articles to [email protected] and share with the Swoole community

Notice: ext-swoole is supported until v4.7.1, use ext-openswoole >= v4.7.1. Latest version: pecl install openswoole-4.8.1