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]