swoole / swoole-src

πŸš€ Coroutine-based concurrency library for PHP
https://www.swoole.com
Apache License 2.0
18.32k stars 3.16k forks source link

Proposal: customizing coroutine context switching #4197

Open sshymko opened 3 years ago

sshymko commented 3 years ago

The Problem

Swoole runtime is by design incompatible with FastCGI mechanisms. Global state specific to FastCGI is out of scope of Swoole. Such architecture is clean, but it greatly limits use cases of Swoole by applications/frameworks designed for PHP-FPM runtime. It is generally possible to retrofit such applications for compatibility with Swoole, but they remain conceptually incompatible with Swoole coroutines (because global context not managed by Swoole is shared between coroutines).

Proposed Solution

Introduce events to track coroutine context switching with ability to preserve custom state.

Events:

Usage Examples

Request Context

Compatibility of super-global variables with Swoole coroutines.

$server->on('request', function ($request, $response) {
    $_GET = (array)$request->get;
    $_POST = (array)$request->post;
    $_FILES = (array)$request->files;
    $_COOKIE = (array)$request->cookie;
    $_REQUEST = $_COOKIE + $_POST + $_GET;
    // ...
});

$server->on('workerSuspend', function () {
    // Return state associated with the current coroutine
    return [$_GET, $_POST, $_FILES, $_COOKIE, $_REQUEST];
});

$server->on('workerResume', function ($server, $workerId, $data) {
    // Restore state associated with the current coroutine
    [$_GET, $_POST, $_FILES, $_COOKIE, $_REQUEST] = $data;
});

Sessions

Compatibility of the native PHP sessions with Swoole coroutines.

$server->on('request', function ($request, $response) {
    session_start();
    try {
        // ...
    } finally {
        session_commit();
    }
});

$server->on('workerSuspend', function () {
    $data = [session_id(), $_SESSION];
    session_abort();
    return $data;
});

$server->on('workerResume', function ($server, $workerId, $data) {
    [$sessionId, $sessionData] = $data;
    session_id($sessionId);
    session_start();
    $_SESSION = $sessionData;
});

That will make libraries, such as upscale/swoole-session, compatible with Swoole coroutines.

twose commented 3 years ago

Have you tried to make global vars be ArrayObject (or something that implements ArrayAccess)? Then you can get coroutine id and find coroutine context by it on ArrayObject::offsetGet().

I am not sure if it's ok to use session_* in coroutines, because your application may be blocked on it when it access the file system...

sshymko commented 3 years ago

@twose Very clever idea to use ArrayAccess interface to scope the data by coroutine ID! This should work at least in some cases, such as for the request super-global variables. How to retrieve the coroutine ID? And where to store the scoped values, in $_GLOBALS?

I'm afraid there going to be cases when scoping the data by coroutine ID is not going to be enough. That includes every scenario when an environment cleanup is needed when switching between coroutines. For instance, session_abort() call cleans up the process for subsequent fresh call to session_start(). The coroutine switching events are necessary to handle cases like this.

twose commented 3 years ago

Very clever idea to use ArrayAccess interface to scope the data by coroutine ID! This should work at least in some cases, such as for the request super-global variables. How to retrieve the coroutine ID? And where to store the scoped values, in $_GLOBALS?

You can use Coroutine::getContext(), it returns ArrayObject and you can store anything in it. You can implement context manager by yourself, I found an example here: https://github.com/hyperf/hyperf/blob/master/src/utils/src/Context.php

About session functions... I don’t like it, I would rather use the session I implemented... and I don't know much about it so I also don't know how to solve it...

ValiDrv commented 3 years ago

You should not really use those super globals even without Swoole. But when you switch to Swoole, you need to wrap them.

Example:

class Context
{
    public static function set(string $key, mixed $value)
    {
        Co::getContext()[$key] = $value;
    }
    // Navigate the coroutine tree and find where we set `key`
    public static function get(string $key, mixed $default = null): mixed
    {
        $cid = Co::getcid();
        do {
            if (isset(Co::getContext($cid)[$key])) {
                return Co::getContext($cid)[$key];
            }
            $cid = Co::getPcid($cid);
        } while ($cid !== -1 && $cid !== false);

        # Not found
        return $default ?? throw new InvalidArgumentException("Could not find `{$key}` in context.", 404);
    }
}

...
$server->on('request', function ($request, $response) {
   Context::set('_GET', (array)$request->get);
   Context::set('_POST', (array)$request->post);
   Context::set('_FILES', (array)$request->files);
   Context::set('_COOKIE', (array)$request->cookie);
   ...
  # And when you use them
  echo Context::get('_GET')['foo'] ?? 'bar;
  # instead of
  echo $_GET['foo'] ?? 'bar;
});

If you do the context switching as you ask, it will slow down coroutines (and 99% of the time you don't need that data)

The only improvement I would do in Swoole would be that Context::get so it's closer to O1

sshymko commented 3 years ago

@ValiDrv Thanks for providing a code example of what the Swoole coroutine context @twose was talking about!

Just wanted to clarify the use of super-globals. If you're designing your application from scratch, yes, you can avoid relying on them altogether. However, when you're adopting an existing PHP-FPM application/framework to work with Swoole, you don't have much choice but to emulate the global FastCGI environment.

Regarding the performance impact, it should be negligible: when observers have been registered the overhead is to execute them when coroutine context switches, otherwise, it's just one extra condition evaluation.

Looks like the proposed approach will work for request super-globals. What would you recommend doing for $_SESSION that requires cleaning/restoring the session context (via session_abort() and session_start()) when coroutines switch?

ValiDrv commented 3 years ago

Normally you roll your own session, so you set it to some context variable on request, save it after the request.

So you don't call the PHP session_ functions.

fakharak commented 1 year ago

@twose @ValiDrv I agree the proposal of @sshymko though not in the context of the "blocking" session, but for the transition of existing PHP-FPM based frameworks to Swoole's concurrency model.

Problem Statement:

The problem with new concurrency mode in Swoole as set by Co::set(['max_concurrency' => 1, ]); is that while it makes the Worker Process behave more like PHP-FPM, it also encourage the traditional PHP-FPM frameworks like "Laravel-S" and "Laravel Octane" to become more reluctant in the use of coroutines (as currently they strictly prohibit the use of Coroutine even inside Worker Processes whereas Coroutine is the base of Asynchronous Programming Paradigm and without Coroutines the purpose of using Asynchrony dies), instead of trying to make transition to Coroutines.

These frameworks then ab-use Swoole to provide hacks (ways around) only for the sake of over-simplification. For example, Laravel Octane uses Task Workers for Concurrency as a replacement of Coroutines which is wrong approach and against the core philosophy of Asynchrony.

Laravel-S (on its Readme.md on github) clearly prohibits the use of Coroutines.

Solution:

Instead of Swoole change its concurrency mode for traditional PHP-FPM based frameworks, a better solution could be an implicit Context Management (built internally in Swoole) such that traditional PHP-FPM based frameworks can specify those Singleton the state of which is mutable per request.

And, then Swoole's built-in Context Manager implicitly inject the data of those mutable Singleton/s and also inject the data of the request. To implement such mehcanism Swoole may make use of events like below:

$server->on('requestSuspend', function () {}); // Third-party Vendor can supply / hook mutable Singletons here, so that Swoole can store their state, implicitly, through context Manager

$server->on('requestResume', function () {}); // Swoole implicitly injects the state back to those singleton and request object using Coroutine Context.

ValiDrv commented 1 year ago

I think that would defeat the purpose of those 3rd party libraries... since they MUST be made aware of the Swoole/Coroutines and if they were designed like that in the first place, you would not need this functionality.

And I think it will be very slow to re-set all the static/global variables on every coroutine change...

sshymko commented 1 year ago

+1 to @fakharak's comment above!

The the ideal solution in the most general form is ability to switch arbitrary globals (mainly to replicate FastCGI/PHP-FPM behavior) when switching coroutines. Without such a mechanism, all attempts to retrofit "classical" frameworks/applications for compatibility with Swoole end up with disabled coroutines altogether which is very unfortunate.

fakharak commented 1 year ago

What i understand is that the new changed Design Model for PHP-FPM based PHP-Frameworks (which currently run with "one-process-per-request" model) can be that the Route / Singletons / Services should now be "non-singleton" Objects created (locally) in callback for Swoole onRequest (local to the Request's coroutine-context), and should also be initialized in the same callback using the Swoole\Http\Request Object.

That proposal is based on two assumptions about Swoole to be valid, as below: 1) Swoole's pre-emptive Scheduler switches the coroutine only when there is a I/O-Task waiting for response.

2) When a Request (coroutine) resumes (after some I/O-task completes in it), then all of the objects, which were created (locally) inside that coroutine / context, become available to the coroutine again along with their original State which they had before the IO-Task started (which causes switching to another request / coroutine).