🐝

PHPで2015年に報告されたバグを踏んでしまった話

2024/11/12に公開

結論。ちゃんとサポートされているPHPのバージョンを使いましょう。

最近作っているもの

最近、勉強を兼ねてPHPのtestcontainers実装を作ってます。

https://github.com/k-kinzal/testcontainers-php

PHPにもtestcontainers-phpというものが存在しているのですが、サポートしているPHPのバージョンが8.1以降のみ。
PHPのサポート状況としては間違っていないのですが、こういうテストで利用するライブラリを本当に必要とするのはもっと古いバージョンなんだよなぁ。というので、shivammathur/phpの一番古いバージョンの5.6から最新の8まで動くものを作っています。

とりあえずMySQLを起動して疎通できるところまではできてる!
(まだちゃんと型とテスト作り込んでないので変な挙動するかも)

PDOConnectWaitStrategy

testcontainersにはWaitStrategyという素敵な概念があり、コンテナ起動から実際に利用するまで待つことができる機能があります。
だいたいはポートの疎通確認で十分なのですが、MySQLは経験上クライアントから接続に成功するまで待った方がテストが安定する印象です。

というのでせっかくPHP使ってるしPDOで接続待機できると良さそうなのでPDOConnectWaitStrategyを実装しました。

https://github.com/k-kinzal/testcontainers-php/blob/main/src/Containers/WaitStrategy/PDO/PDOConnectWaitStrategy.php

while (1) {
    if (time() - $now > $this->timeout) {
        throw new WaitingTimeoutException($this->timeout);
    }
    try {
        $pdo = new PDO($dsn->toString(), $this->username, $this->password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 1,
        ]);
        $pdo->query('SELECT 1');
        $pdo = null;

        break;
    } catch (PDOException $e) {
        // Do nothing
    }
    usleep($this->retryInterval);
}

とりあえずこんな感じにPDOをnewして接続して、失敗したら繰り返せばいいやというスタンスです。
さくっとできたのでさくっとテストで確認してみよ!とPHP5.6環境で動かしてみたところ・・・

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 4104 bytes) in /Users/me/Projects/testcontainers-php/src/Containers/WaitStrategy/PDO/PDOConnectWaitStrategy.php on line 96

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4104 bytes) in /Users/me/Projects/testcontainers-php/src/Containers/WaitStrategy/PDO/PDOConnectWaitStrategy.php on line 96

プロセスは終了コード 255 で終了しました

_人人人人人_
> なんで <
 ̄Y^Y^Y^Y^Y^ ̄

PDOのバグの発見

最初はループの中でGCが動いてないのかと思いgc_collect_cyclesを呼び出したり、$pdoに中途半端に何か確保されているのか?と空を設定したり、明示的にunsetを呼び出したり、whileループ内だと解放処理うまくいかないのかと関数に切り出してスコープから外すようにしたり、例外で抜けるのが悪いのかとPDO::ERRMODE_SILENTを設定したりと本当に右往左往していました。
それでもどうにもならず、ChatGPTに聞いたり、Google先生に聞いたりと調べているとついに見つけました。

https://bugs.php.net/bug.php?id=70901

Memory Leak!!!

どうやらPDOではメモリを解放するのはデストラクタでのみであり、コンストラクタで例外が発生するとデストラクタが呼ばれずにメモリリークが発生してしまうようです。

PHP7以降では大丈夫なのか

これ何が怖いってStatus: Wont fixで止まっていることなんですよね。
ざっとPDOのコードを読んでみても明示的に解放する処理入ってないし、もしかして今のバージョンでも・・・と思って7、8で試したところ大丈夫でした。

セーフ!!!

さすがにPHPのコードの方まで追う元気はないので掘り下げないですが、きっと7以降で何かが変わったんだろうなと一安心です。よかった。

おわりに

軽い気持ちでPHP5.6サポートをやってしまいましたが、タイプヒント使えないし、PHPStanは使えないし、2015年に報告されたバグは踏むしで何のいいこともありませんね!!
と言いつつも、僕はPHP5.xが一番長く触っていたので懐かしい気持ちでいっぱいです。

それにしてもPHP8ではクラスを使って型安全になるように組むのに、今でもPHP5.6を触ると配列をゴリゴリ使って型安全何それになるのは何でなんでしょうね。
何というかPHP5.6ではこう書かねばならないというDNAに刻まれた何かがありそうです。

Discussion