Hi,

On Fri, Nov 21, 2025 at 8:16 AM Edmond Dantes <[email protected]> wrote:

> Hello.
>
> Imagine that we have an application like this.
>
> ```php
> class AuthService
> {
>     private static ?self $instance = null;
>
>     private PDO $db;
>     private ?string $sessionId = null;
>
>     // Private constructor for singleton
>     private function __construct(PDO $db)
>     {
>         $this->db = $db;
>     }
>
>     // Get singleton instance
>     public static function getInstance(PDO $db): self
>     {
>         if (self::$instance === null) {
>             self::$instance = new self($db);
>         }
>         return self::$instance;
>     }
>
>     public function login(string $email, string $password): bool
>     {
>         // Find user by email
>         $stmt = $this->db->prepare('SELECT * FROM users WHERE email = ?');
>         $stmt->execute([$email]);
>         $user = $stmt->fetch(PDO::FETCH_ASSOC);
>
>         // Invalid credentials
>         if (!$user || !password_verify($password, $user['password_hash']))
> {
>             return false;
>         }
>
>         // Generate and save session ID
>         $this->sessionId = bin2hex(random_bytes(16));
>
>         $stmt = $this->db->prepare(
>             'INSERT INTO sessions (user_id, session_id) VALUES (?, ?)'
>         );
>         $stmt->execute([$user['id'], $this->sessionId]);
>
>         return true;
>     }
>
>     // Return current session ID
>     public function getSessionId(): ?string
>     {
>         return $this->sessionId;
>     }
> }
> ```
>
> One day you decide you want more performance and make a single PHP
> process handle multiple connections concurrently. You wrap each
> request in a separate coroutine and try to use the old code.
>
> ```php
> $server = new Swoole\Http\Server("127.0.0.1", 9501);
>
> $server->on("request", function ($req, $res) {
>
>     // create DB connection (just for example)
>     $db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
>
>     // get singleton
>     $auth = AuthService::getInstance($db);
>
>     // read request data
>     $data = json_decode($req->rawContent(), true);
>
>     $email = $data['email'] ?? '';
>     $password = $data['password'] ?? '';
>
>     // call old sync code
>     $ok = $auth->login($email, $password);
>
>     if ($ok) {
>         $res->end("Logged in, session: " . $auth->getSessionId());
>     } else {
>         $res->status(401);
>         $res->end("Invalid credentials");
>     }
> });
>
> $server->start();
> ```
>
> What is happening here?
> Now, in PHP, inside a single process or thread, the same code is
> literally handling multiple connections.
> At the same time, there are constant switches between different
> requests at the points where MySQL queries occur.
>
> That is, when the code executes
> $stmt->execute([$email]);
> control is passed to another coroutine with a different
> $stmt->execute([$email]);
>
> What breaks in this code?
> Correct, coroutines break the singleton because they alternate writing
> different Session IDs!
>
>
I think you seriously underestimate impact of this in the current PHP code
bases where many applications depend on global state. Especially the legacy
ones but even the most popular ones. Just look into WordPress which use
global state extensively. Now imagine that some popular plugin decides to
use async which change some of its globals (like $post or $wp_query) during
the suspension of the main code. I would assume this could horribly break
things. Don't forget that other code don't have control over the plugin and
it might not even know that async is used there. So I'm not sure if this
design is compatible with WordPress and similar applications where global
state is used extensively. If that's the case, it's of course a problem
because those applications (well WordPress on its own in fact) compose the
majority of PHP users so introducing something that would have potential to
break its code would limit usability of the whole feature and could even
lead to loosing more users that we could gain from introducing this feature.

So I think it will need to find some solution that will prevent this from
happening. I guess there might be few options

1. Disallow suspension of the main sync code which is effectively some sort
of colouring.
2. Preventing access to globals from coroutine which I'm not sure is even
fully doable from the engine PoV - it would mean some sort of different
execution mode that could not use globals (e.g. global keyword and calling
some functions that change global state). It would need channels for
communications between coroutines. The advantage of such model would be
possibility to combine it with threads in the future but I could imagine it
could still lead to some subtle issue for years as there is internal global
state as well that can lead to some surprises. But maybe that would be
worth it.

Just some thoughts...

Cheers

Jakub

Reply via email to