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!

And what does not change in this code?
The SQL queries can remain unchanged.

The first problem with shared memory between coroutines can ONLY be
solved by the programmer. Only the programmer. There is no solution
that would make this happen automatically.
Yesterday we talked about how we can help the programmer detect such
situations during debugging. But in any case, only the programmer
**CAN** and **MUST** solve this problem.

The difference is that you don’t need to rewrite everything else.
The focus is only on the issue of concurrent access to memory.

The essence of the choice is how much code needs to be rewritten.
Almost everything, or only the code with global state.
My choice is: it’s better to rewrite only the code with global state —
or switch to Go and avoid the pain :)

As for the rest, I will write a separate message so as not to clutter things up.

----
Edmond

Reply via email to