phasync
Context switching / Why code breaks when requests overlap

Context switching

Why code breaks when requests overlap

Under PHP-FPM a request owns its process. A Swerve worker serves many requests, and what they reach through a global is shared.

Under PHP-FPM a process serves one request and starts it with nothing in memory. Code written for that keeps request data wherever it is convenient: a global, a static property, a static variable in a function, a singleton that a service locator hands out. None of it is shared with another request, because the process belongs to one.

A Swerve worker keeps its memory between requests and serves several at once. A request that waits (for a query or an HTTP call; with the phasync extension also sleep()) lets the worker run another. Whatever both reach through a global is now shared.

final class CurrentUser
{
    public static ?string $name = null;
}

function handle(string $name): string
{
    CurrentUser::$name = $name;      // log the user in
    phasync::sleep(0.01);            // the request waits, for a query say
    return "hello " . CurrentUser::$name;
}

phasync::run(function () {
    // Two overlapping requests; Swerve runs each one in a Scope of its own, like these
    $ann = (new Scope())->run(fn () => phasync::go(fn () => handle('ann')));
    $bob = (new Scope())->run(fn () => phasync::go(fn () => handle('bob')));
    echo phasync::await($ann), "\n", phasync::await($bob), "\n";
});
hello bob
hello bob

Ann's request answers as Bob. Code between two waits runs without being interrupted, so the bug shows only when a request waits between writing the state and reading it. A test that sends one request at a time passes.

What is safe#

Finding it#

The checklist in making your application safe for concurrent requests goes through these patterns one by one, with a fix for each.

Previous: Learn ยท Next: Per-request state

See also#