On 06/03/2025 11:31, Edmond Dantes wrote:
For example, PHP has functions for working with HTTP. One of them writes the last received headers into a "global" variable, and another function allows retrieving them. This is where a context is needed.


OK, let's dig into this case: what is the actual problem, and what does an async design need to provide so that it can be solved.

As far as I know, all current SAPIs follow one of two patterns:

1) The traditional "shared nothing" approach: each request is launched in a new process or thread, and all global state is isolated to that request. 2) The explicit injection approach: the request and response are represented as objects, and the user must pass those objects around to where they are needed.

Notably, 2 can be emulated on top of 1, but not vice versa, and this is exactly what a lot of modern applications and frameworks do: they take the SAPI's global state, and wrap it in injected objects (e.g. PSR-7 ServerRequestInterface and ServerResponseInterface).

Code written that way will work fine on a SAPI that spawns a fiber for each request, so there's no problem for us to solve there.

At the other extreme are frameworks and applications that access the global state directly throughout - making heavy use of superglobal, global, and static variables; directly outputting using echo/print, etc. Those will break in a fiber-based SAPI, but as far as I can see, there's nothing the async design can do to fix that.

In the middle, there are some applications we *might* be able to help: they rely on global state, but wrap it in global functions or static methods which could be replaced with some magic from the async implementation.


So our problem statement is:

- given a function that takes no request-specific input, and is expected to return request-specific state (e.g. function get_query_string_param(string $name): ?string)
- and, given a SAPI that spawns a fiber for each request
- how do we adjust the implementation of the function, without changing its signature?


Things we don't need to define:

- how the SAPI works
- how the data is structured inside the function

Non-solutions:

- refactoring the application to pass around a Context object - if we're willing to do that, we can just pass around a PSR-7 RequestInterface instead, and the problem goes away


Minimal solution:

- a way to get an integer or string, which the function can use to partition its data

Usage example:

function get_query_string_param(string $name): ?string {
    global $request_data; // in a shared-nothing SAPI, this is per-request; but in a fiber-based one, it's shared between requests     $request_data_partition = $request_data[ Fiber::getCurrent()->getId() ]; // this line makes the function work under concurrent SAPIs     return $request_data_partition['query_string'][$name]; // this line is basically unchanged from the original application
}


Limitation:

- if the SAPI spawns a fiber for the request, but that fiber then spawns child fibers, the function won't find the right partition

Minimal solution:

- track and expose the "parent" of each fiber

Usage example:

function get_query_string_param(string $name): ?string {
    global $request_data;
    // Traverse until we find the ID we've stored data against in our request bootstrapping code
    $fiber = Fiber::getCurrent();
    while ( ! isset($request_data[ $fiber->getId() ] ) {
        $fiber = $fiber->getParent();
    }
    $request_data_partition = $request_data[ $fiber->getId() ];
    return $request_data_partition['query_string'][$name];
}


Obviously, this isn't the only solution, but it is sufficient for this problem.

As a first pass, it saves us bikeshedding exactly what methods an Async\Context class should have, because that whole class can be added later, or just implemented in userland.

If we strip down the solution initially, we can concentrate on the fundamental design - things like "Fibers have parents", and what that implies for how they're started and used.


--
Rowan Tommins
[IMSoP]

Reply via email to