超長い文字列や配列でも実体が同じなら中身の大きさには依存しない

手元にあった PHP 7.1.10 で試してます。


PHP の配列で、連想配列なのかいわゆる添字が順序通りのただの配列なのかを調べるには、下記の方法が一番手っ取り早いと思います。

array_values($arr) === $arr;

がしかし、配列の === は配列の要素の値の比較も行われるため、これだと配列の中に超でかい文字列や配列が入っているときに超遅くなります。

比較する必要があるのはキーだけなはずなのに配列の要素の値に依存して実行時間が変わるのはイケていないので array_keys でキーだけ取り出して比較したり愚直に foreach で回す方が良いですね・・・そんなふうに考えていた時期が俺にもありました。

いろんなパターンでベンチして最適な方法さがすぞー!と意気込んだところ↑のパターンが概ね良好でした(要素数が少ないときや早期にミスマッチが判断できるときは foreach のが早いこともある)。

<?php
namespace array_values {
    function isSequence($arr)
    {
        return $arr === array_values($arr);
    }
    $namespaces[] = __NAMESPACE__;
}

namespace array_keys {
    function isSequence($arr)
    {
        return array_keys($arr) === array_keys(array_keys($arr));
    }
    $namespaces[] = __NAMESPACE__;
}

namespace range {
    function isSequence($arr)
    {
        if (count($arr) === 0) {
            return true;
        } else {
            return array_keys($arr) === range(0, count($arr) - 1);
        }
    }
    $namespaces[] = __NAMESPACE__;
}

namespace foreach_ {
    function isSequence($arr)
    {
        $j = 0;
        foreach ($arr as $i => $v) {
            if ($i !== $j++) {
                return false;
            }
        }
        return true;
    }
    $namespaces[] = __NAMESPACE__;
}

namespace {
    function test($ns)
    {
        $func = "$ns\\isSequence";

        assert($func([]) === true, "$func([]) === true");
        assert($func([2, 4, 6]) === true, "$func([2, 4, 6]) === true");
        assert($func([2, 4, 'x' => 6]) === false, "$func([2, 4, 'x' => 6]) === false");
        assert($func([2 => 2, 1 => 4, 0 => 6]) === false, "$func([2 => 2, 1 => 4, 0 => 6]) === false");
    }

    $data = [
        '[]' => [],
        '[2,4,6]' => [2, 4, 6],
        '[x:9]' => ['x' => 9],
        '[1:1,0:0]' => [1 => 1, 0 => 0],
        '[0..10000]' => range(0, 10000),
        '{0..10000, x:x}' => range(0, 10000) + ['x' => 'x'],
        '[0, 0..100000]' => [0, range(0, 100000)],
        '{x:x, z: 0..100000}' => ['x' => 'x', 'z' => range(0, 100000)],
        '[0, x..x]' => [0, str_repeat("x", 100000)],
        '{x:x, z: x..x}' => ['x' => 'x', 'z' => str_repeat("x", 100000)],
    ];

    foreach ($namespaces as $ns) {
        test($ns);
    }

    foreach ($namespaces as $ns) {
        $results = [];
        foreach ($data as $name => $arr) {
            $func = "$ns\\isSequence";
            for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
                $func($arr);
            }
            printf("%-16s%-24s%d\n", $ns, $name, $i);
            $results[] = $i;
        }
        printf("%-16s%-24s%d ... %d\n", $ns, "", min($results), max($results));
    }
}
array_values    []                      3947641
array_values    [2,4,6]                 3383804
array_values    [x:9]                   3956602
array_values    [1:1,0:0]               3893512
array_values    [0..10000]              5912
array_values    {0..10000, x:x}         6079
array_values    [0, 0..100000]          3246093
array_values    {x:x, z: 0..100000}     3820617
array_values    [0, x..x]               3392908
array_values    {x:x, z: x..x}          3945126
array_values                            5912 ... 3956602
array_keys      []                      3104513
array_keys      [2,4,6]                 2405964
array_keys      [x:9]                   2596304
array_keys      [1:1,0:0]               2524770
array_keys      [0..10000]              3455
array_keys      {0..10000, x:x}         3429
array_keys      [0, 0..100000]          2456517
array_keys      {x:x, z: 0..100000}     2541887
array_keys      [0, x..x]               2523583
array_keys      {x:x, z: x..x}          2530141
array_keys                              3429 ... 3104513
range           []                      5123887
range           [2,4,6]                 1966400
range           [x:9]                   2065069
range           [1:1,0:0]               2093647
range           [0..10000]              4480
range           {0..10000, x:x}         4243
range           [0, 0..100000]          1963215
range           {x:x, z: 0..100000}     2149248
range           [0, x..x]               2077303
range           {x:x, z: x..x}          2039283
range                                   4243 ... 5123887
foreach_        []                      5173692
foreach_        [2,4,6]                 3569323
foreach_        [x:9]                   4653941
foreach_        [1:1,0:0]               4681362
foreach_        [0..10000]              4177
foreach_        {0..10000, x:x}         4102
foreach_        [0, 0..100000]          4032628
foreach_        {x:x, z: 0..100000}     4661640
foreach_        [0, x..x]               4007409
foreach_        {x:x, z: x..x}          4776680

実体が同じなら文字列や配列の中身は比較されない

ということのようです。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = str_repeat('x', 1024*1024*10);
$b = $a;
func($a, $b, 'same huge str');

$a = str_repeat('x', 1024*1024*10);
$b = str_repeat('x', 1024*1024*10);
func($a, $b, 'diff huge str');

$a = 'x';
$b = 'x';
func($a, $b, 'diff tiny str');
  11970864 ... same huge str
       782 ... diff huge str
  11892552 ... diff tiny str

最初のパターンはちょうでかい文字列ですけど実体が同じ変数の比較なので1文字の文字列の比較と差がありません。 一方で2番目のパターンは同じ文字列ではあるものの実体が異なるので超遅いです。

配列でも同様です。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = range(1, 1000000);
$b = $a;
func($a, $b, 'same huge arr');

$a = range(1, 1000000);
$b = range(1, 1000000);
func($a, $b, 'diff huge arr');

$a = [];
$b = [];
func($a, $b, 'diff tiny arr');
  11500321 ... same huge arr
        82 ... diff huge arr
  11197598 ... diff tiny arr

PHP の変数は Copy on Write になっており単に = でコピーしただけなら実際に確保されている文字列や配列のためのメモリ領域は共有されており、なんらかの変更を行ったときに実際の領域がコピーされます。

そして、2つの変数を比較するとき、変数の中身が同じ実体で同じ領域を指しているのであれば、中身を比較するまでもなく「一致」と判断することができるのでしょう(ソースは見ていないけどたぶん)。

配列のキーも文字列のサイズには依存しない

PHP の配列はハッシュテーブルなので文字列をキーにする場合は文字列を元にハッシュ値を計算する必要があります。なので、あまりに巨大な文字列をキーにすると計算のコストが増大して性能が劣化する・・・そんなふうに考えていた時期が俺にもありました。

<?php
function func($arr, $key, $name)
{
    $arr[$key] = 1;
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $v = $arr[$key];
    }
    printf("%10d ... %s\n", $i, $name);
}

$key = str_repeat('x', 1024*1024*10);
$arr[$key] = 1;
func($arr, $key, 'same huge str');

$key = str_repeat('x', 1024*1024*10);
$arr[$key . ''] = 1;
func($arr, $key, 'diff huge str');

$key = 'x';
$arr[$key . ''] = 1;
func($arr, $key, 'diff tiny str');
  11339929 ... same huge str
       781 ... diff huge str
  11241169 ... diff tiny str

最初のパターンは超でかい文字列をキーにしているのでハッシュ値の計算のために性能が劣化しそうなものですが、キーが短いときと代わりありません。

文字列自体が配列のキーとして使うときのためのハッシュ値を持っているとでもいうの・・・

zend_stringに関するメモ - Qiita

struct _zend_string {
        zend_refcounted_h gc;
        zend_ulong        h;                /* hash value */
        size_t            len;
        char              val[1];
};

あ、持ってるっぽい、なるほど。

PHP 5.4.16

まだまだ現役?? の PHP 5.4.16 で試してみました。

# str.php
      1801 ... same huge str
       816 ... diff huge str
   5962900 ... diff tiny str

# arr.php
   5819125 ... same huge arr
        47 ... diff huge arr
   5843144 ... diff tiny arr

# key.php
        85 ... same huge str
        84 ... diff huge str
   6210093 ... diff tiny str

配列の比較だけは PHP 7 と同じような傾向ですが、文字列の比較と配列のキーはご覧の有様でした。

さいごに

PHP 7 なめてた。

例えば PSR-7 を実装したリクエストオブジェクトのアトリビュートに何か入れるとき、名前の競合を避けるためにクラス名を使いたかったりすることあるんですけど、

$request = $request->withAttribute(HogeAttr::class, new HogeAttr($hoge));

今日日の PHP は名前空間が付いていてクラス名の完全修飾名は長くなりがちなので、リクエストオブジェクトの中でアトリビュート名が配列のキーとして持たれることを考えるとクラス名よりも短い文字列の定数とかを別に用意するのが良いだろうかと思ったのだけど・・・

$request = $request->withAttribute(HogeAttr::NAME, new HogeAttr($hoge));
// or
$request = $request->withAttribute('hoge', new HogeAttr($hoge));

そんなこと考える必要は無いということですね。むしろクラスがロードされた時点で存在していることが明らかであるクラス名の文字列の方が好ましいでしょう(::class が使えるならですけど)。

DI コンテナのオブジェクトの ID も同じです。PSR-11 で ID にはクラス名やインタフェース名を使うのを奨励、みたいなことが書かれてた気がしますが、PHP 7 ならクラス名やインタフェース名が超長くてもノーコストなのですね(コンテナの実装に依る)。

DIコンテナ使ってみて思った雑文:その2

アプリケーションにテーブルゲートウェイがあったとして、もちろんDB接続に依存している。

class UserTable
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

テーブルゲートウェイはテーブルの数だけ存在するので、全部に同じ実装を書くのはDRYじゃないので抽象クラスを作る。

abstract class AbstractTableGateway
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

class UserTable extends AbstractTableGateway
{
    // UserTable spesific method...
}

特定のテーブルで Connection 以外のなにかに依存する場合、コンストラクタを拡張して DI する。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Hoge $hoge)
    {
        parent::__construct($connection);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

おっと、ここでテーブルのメタデータを保存するために AbstractTableGateway でキャッシュを使いたくなった。

abstract class AbstractTableGateway
{
    private $connection;
    private $cache;

    public function __construct(Connection $connection, Cache $cache)
    {
        $this->connection = $connection;
        $this->cache = $cache;
    }

    // select/insert/update/delete/etc...
}

すると HogeTable のコンストラクタも弄らなければならない。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Cache $cache, Hoge $hoge)
    {
        parent::__construct($connection, $cache);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

HogeTable 的には「キャッシュとか知らんがなそっちで勝手にやってくれ」なので HogeTable のコンストラクタにまで影響するのは違和感がある。

これはまあ Connection と Cache を併せたなにかを作るか(ConnectionFacade とか)、あるいは、Connection に Cache のインスタンスを持たせればスッキリする。

あるいは AbstractTableGateway のような抽象クラスを作って継承するのはやめて、委譲とかトレイトだけでどうにかするべきだろうか。

trait TableGatewayTrait
{
    abstract protected function getTableGateway();

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait;

    private $tableGateway;
    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->tableGateway = $tableGateway;
        $this->hoge = $hoge;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // HogeTable spesific method...
}

コンストラクタとかで定形パターンをなんども書くのが辛ければそれらもトレイトに含めて、必要に応じてコンストラクタをオーバーライドするとか。

trait TableGatewayTrait
{
    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait { __construct as constructTableGateway; }

    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->constructTableGateway($tableGateway);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

コンストラクタを as で別名にするのはさすがに変か。。。


コンストラクタインジェクション前提で考えると、継承元のコンストラクタを弄ったときに派生先のすべてに影響してしまうので継承させにくいなーと思ったけどよく考えたら DI とか関係なく継承してれば当たり前のことだった。

Terraform v0.10 でいろいろ変わってた

久しぶりに Terraform を使ってたらちょこちょこ変わってたのでメモ。多分前は v0.8 とかを使ってたと思う。

Initialization

以前は terraform をインストールすると AWS とかのプロバイダのプラグインも一緒に入っていて、すぐに使えていたと思うんですが、いつからか terraform init コマンドでプロバイダのプラグインがインストールされるようになったようです。

例えば次のように AWS のリソースを tf ファイルに書いておくと。

resource "aws_vpc" "test" {
    cidr_block = "10.1.0.0/16"
    tags { Name = "test" }
}

terraform init.terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4 のような位置に AWS プロバイダのためのプラグインがインストールされます。

$ terraform init

... snip ...

* provider.aws: version = "~> 0.1"

... snip ...

$ ls .terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4
.terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4

後はこれまで通り plan とか apply とかできます。

ちなみにこのファイルはいわゆる実行ファイルなので、実行属性を落とすと動かなくなります。

Mac とかなら大丈夫なんですけど・・・普段 Windows を cifs マウントしている Linux 上で作業している自分にはその仕様はちょっとツライ・・・ mound --bind とかでどうにか乗り切ってますけど。

Remote Backends

Remote Backend の設定方法も以前から変わっているようで、以前のまま使うと「なんか古いから新しくしいや」みたいなことを言われた気がします。

以前は terrarofm remote みたいなコマンドでバックエンドを設定する必要があった気がするのですが、今は次のように tf ファイルにバックエンドの情報を書いて、

terraform {
    backend "s3" {
        bucket = "oreore-tfstate"
        key    = "oreore.tfstate"
    }
}

terraform init で設定出来るようです。

複数の作業者で共有すること考えたらこの方が良いですね。

バックエンドを変更したりローカルに戻したりも tf ファイルを修正して terraform init で良いようです。楽ちん。

DIコンテナ使ってみて思った雑文

アプリケーションの流れとして、

  1. コンテナを構築
  2. アプリを実行するなにか(パイプラインとかディスパッチャとか)をコンテナから取り出す
  3. リクエストオブジェクトを作成してアプリを実行

みたいな流れかなと思う。順番的にDIコンテナが先に作られるのでリクエストオブジェクトやリクエストに含まれる情報はDIコンテナには入らない。そもそもDIコンテナはグローバルなコンテキストのものなので、リクエストのコンテキストに属するオブジェクトを入れるべきではない。

(PHPでは出来ないけれども)もし可能なのならDIコンテナはリクエストをまたがって共有されても良い。なのでコンテナに入るオブジェクトはステートレスでなければならないし、再入可能でスレッドセーフであるべき(PHPでは関係ないけれども)。

・・・みたいなイメージ。


例えば Logger にリクエストに基づく情報が自動的に付加されるようにしてみる(IPアドレスとか)。

$logger = $logger->withContext(['ipaddr' => $ipaddr]);

// $context に 'ipaddr' が追加される
$logger->info('ほげほげ');

こうして作られた Logger はリクエストのコンテキストに属するので、グローバルなコンテキストであるところのコンテナには入らない。

サービスクラスがコンテナに入っているのだとすると、サービスクラスのコンストラクタにインジェクションできるものはコンテナに入っているものだけなので、↑のよう部分的に適用された Logger をインジェクションすることはできない。

大本の Logger がインジェクションされて、そしてサービスのメソッドにリクエストオブジェクトが渡されるようにする?(コードはイメージです)

class HogeService
{
    public function __construct($logger)
    {
        $this->logger = $logger
    }

    public function func($request)
    {
        $logger = $this->logger->withContext(['ipaddr' => $request->getRemoteAddr()]);

        // $logger を使ってログを出す
    }
}

ちがう、サービスがリクエストに依存しているわけではないので ipaddr が適用された Logger を渡すべき。

class HogeService
{
    public function func($logger)
    {
        // $logger には 'ipaddr' が適用されているけどサービスはそんなこと知らねえ
    }
}

ただ $logger をずっと引数として持ち回さなければならない。引数が Logger しかないなら良いけれども、他にもリクエストのコンテキストに属するオブジェクトをたくさん持ち回したくなったときにツラミが増してくる(Logger 以外なにがあるかぱっと思いつかないけど)。

ので、それを見越して、リクエストのコンテキスト、というオブジェクトが渡されるようにしてみる。

class HogeService
{
    public function func($requestContext)
    {
        $logger = $requestContext->getLogger();
    }
}

うーん悪手? サービスが RequestContext というよくわからないものに依存している。


Logger がコンストラクタからシュッと入ってきてそれをそのまま使えられれば良いのだけど、コンテナにリクエストに基づくモノが入っていないとすると引数で持ち回すしか無い?

あるいはコンテナにリクエストオブジェクトをぶっこんでも良いことにする?

// bootstrap.php っぽいなにか
$builder = new ContainerBuilder();
$builder->addDefinitions($definitions + [
    ServerRequestInterface::class => ServerRequestFactory::fromGlobals(),
]);

$container = $builder->build();

// コンテナにリクエストオブジェクトが入っているので logger に ipaddr が適用されている
$logger = $container->get(Logger::class);

でも PSR-7 だとミドルウェアパイプラインの中でリクエストオブジェクトがクローンされて次々に生成されるわけで、↑だとコンテナに入ったリクエストオブジェクトとコントローラーのアクションに渡ってくるリクエストオブジェクトが異なるものになってしまって違和感。

例えば X-Forwarded-For を見てリクエストが持つ RemoteAddr を本来のものに書き換えるようなミドルウェアを考えると問題がわかりやすい。

それか Logger をミュータブルにしてそういうミドルウェアが Logger に IP アドレスをセットする?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $this->logger->addContext(['ipaddr' => $request->getRemoteAddr()]);
        return $delegate->process($request);
    }

うーん、どんどんゆるふわになっていく・・


コントローラーをディスパッチするときに元のコンテナを拡張した新たなコンテナを作成して、コントローラーの依存はその新たなコンテナによって解決する?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $container = $this->extend($this->container, [
            LoggerInterface::class => function () use ($request) {
                return (new Logger())->withContext([
                    'ipaddr' => $request->getRemoteAddr(),
                ]);
            }
        ]);

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

リクエストのアトリビュートを全部ぶっこむとかでも。

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $request = $request->withAttribute(
            LoggerInterface::class, (new Logger())->withContext([
                'ipaddr' => $request->getRemoteAddr(),
            ])
        );

        $container = $this->extend($this->container, $request->getAttributes());

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

とくに結論はない。

PHPUnit の Functions.php を自動でロードするやつ

下記↓の記事のコメントの通り、

PHPUnit には Functions.php というファイルが含まれていて、これを require するとアサーションなどがグローバル関数に登録されるので、次のようにテストを書くことができます。

<?php
// 普通こう書くけど、
$this->assertThat($actual, $this->equalTo($expect));

// Functions.php を読んどけばこうも書ける。
assertThat($actual, equalTo($expect));

とても便利なんですけど Functions.php は明示的に読む必要があります。

<?php
require_once __DIR__ . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';

しかもこれ、PHPUnit のバージョンによって違う位置にあります。

さらに、最近なら PHPUnit を Phar で実行することもあると思います。その場合は Phar ファイル名を元に読む必要があります。

<?php
require_once 'phar://phpunit-6.2.phar/phpunit/Framework/Assert/Functions.php';

こういうのが辛かったので、次のようにリフレクションでファイル名を特定するようにしていたのですが・・・PHPUnit 6 でクラス名が PEAR から PSR-4 のスタイルに変わったので、これも動かなくなりました。

<?php
require __DIR__ . '/../vendor/autoload.php';
$reflection = new ReflectionClass('PHPUnit_Framework_Assert');
require_once dirname($reflection->getFileName()) . '/Assert/Functions.php';

そもそも↑のようなスニペットをいちいちコピペするのも面倒臭くなってきたので Functions.php を自動的に読み込むやつを作りました。

次のように composer で追加しておくと、テストの実行時に自動的に Functions.php が読まれます。

composer require --dev ngyuki/phpunit-functions

これは便利。


MySQL で date_add/sub で年が 0000 になると 0000-00-00 が返る

MySQL の sql_modeNO_ZERO_IN_DATE,NO_ZERO_DATE とかを指定すると 0000-00-00 や、月や日が 00 などの日時の挿入を禁止できます。

がしかし、年が 0000 の場合の動きが謎いです。

MySQL のバージョンは次の通り。

select version();
/*-----------+
| version()  |
+------------+
| 5.7.19-log |
+-----------*/

SQL モードを指定します。

set session sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO';
select @@sql_mode;
/*--------------------------------------------------------------------------+
| @@sql_mode                                                                |
+---------------------------------------------------------------------------+
| STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO |
+--------------------------------------------------------------------------*/

データを入れます。想定通りの動きだと思います。月や日の 00 は無効ですが年の 0000 は有効です。

create table t (d date);
insert into t values ('0000-00-00'); /* ERROR 1292 (22007): Incorrect date value */
insert into t values ('0001-01-00'); /* ERROR 1292 (22007): Incorrect date value */
insert into t values ('0001-00-01'); /* ERROR 1292 (22007): Incorrect date value: */
insert into t values ('0000-01-01'); /* Query OK */
insert into t values ('0000-01-02'); /* Query OK */
insert into t values ('9999-12-30'); /* Query OK */
insert into t values ('9999-12-31'); /* Query OK */

なんということでしょう。

select d, date_add(d, interval 1 day) as `d+1` from t;
/*-----------+------------+
| d          | d+1        |
+------------+------------+
| 0000-01-01 | 0000-00-00 |
| 0000-01-02 | 0000-00-00 |
| 9999-12-30 | 9999-12-31 |
| 9999-12-31 | NULL       |
+------------+-----------*/

select d, date_sub(d, interval 1 day) as `d-1` from t;
/*-----------+------------+
| d          | d-1        |
+------------+------------+
| 0000-01-01 | 0000-00-00 |
| 0000-01-02 | 0000-00-00 |
| 9999-12-30 | 9999-12-29 |
| 9999-12-31 | 9999-12-30 |
+------------+-----------*/

9999-12-31 + 1 day が NULL なのは良いとして 0000-01-010000-01-02date_adddate_sub すると 0000-00-00 なりました。

下記の結果を見るに、比較はうまく動いているっぽい。

select d, d < '0000-01-02', d > '0000-01-01' from t;
/*-----------+------------------+------------------+
| d          | d < '0000-01-02' | d > '0000-01-01' |
+------------+------------------+------------------+
| 0000-01-01 |                1 |                0 |
| 0000-01-02 |                0 |                1 |
| 9999-12-30 |                0 |                1 |
| 9999-12-31 |                0 |                1 |
+------------+------------------+-----------------*/

どうやら date_adddate_sub による演算の結果、年が 0000 になると 0000-00-00 になってしまうっぽい。

select date_add('0000-12-30', interval 1 day) as `0000-12-30 + 1`,
       date_add('0000-12-31', interval 1 day) as `0000-12-31 + 1`,
       date_sub('0001-01-01', interval 1 day) as `0001-01-01 - 1`,
       date_sub('0001-01-02', interval 1 day) as `0001-01-02 - 1`;
/*---------------+----------------+----------------+----------------+
| 0000-12-30 + 1 | 0000-12-31 + 1 | 0001-01-01 - 1 | 0001-01-02 - 1 |
+----------------+----------------+----------------+----------------+
| 0000-00-00     | 0001-01-01     | 0000-00-00     | 0001-01-01     |
+----------------+----------------+----------------+---------------*/

西暦1年の前年は0年ではなく紀元前1年?なのなら 0000 という年は無効な日付になるので結果が 0000-00-00 とかになるのも判らなくもないけど・・それなら date_adddate_sub の入力に与えられたときも結果は 0000-00-00 になるべきな気がするし NO_ZERO_IN_DATE で弾かれてほしい気もする。

あとこれ、テーブルへの格納ではなくて date_adddate_sub による演算に対して発生しているので sql_mode は関係なく発生するようです。

KeepAlive On な Apache+mod_php で HTTP/1.0 クライアントに HTTP/1.1 を返すとタイムアウトを待ってしまう

  • PHP 7.1.7
  • Apache 2.4.10
  • ApacheBench 2.3
  • zend-expressive 2.0.3
  • zend-expressive-skeleton 2.0.3
  • zend-diactoros 1.4.0
  • zend-stratigility 2.0.1

Docker で Apache+mod_php を実行して、

docker run --rm -p 8888:80 -v "$PWD:/var/www/html" php:apache

header 関数で HTTP/1.1 を指定するコードを配置して、

<?php
header('HTTP/1.1 200 OK');

次のように ab します。

ab -c 1 -n 1 http://localhost:8888/

すると、なんか異様なスコアになります(1回のリクエストに5秒もかかってる)。

Requests per second:    0.20 [#/sec] (mean)
Time per request:       5011.751 [ms] (mean)
Time per request:       5011.751 [ms] (mean, across all concurrent requests)

header 関数をコメントアウトしたり、

<?php
//header('HTTP/1.1 200 OK');

HTTP/1.0 を指定したり、

<?php
header('HTTP/1.0 200 OK');

Connection: close を指定したり、

<?php
header('HTTP/1.1 200 OK');
header('Connection: close');

Aapche Bench で KeepAlive を有効にすれば、

ab -k -c 1 -n 1 http://localhost:8888/

それっぽい結果になります。

Requests per second:    560.85 [#/sec] (mean)
Time per request:       1.783 [ms] (mean)
Time per request:       1.783 [ms] (mean, across all concurrent requests)

原因

Apache Bench はデフォで HTTP/1.0 なリクエストを送ります。なのでレスポンスの最後はサーバの方からクローズされることを期待します。

一方、php が header 関数で HTTP/1.1 を指定すると、Apache は元のリクエストが 1.0 だったとしても 1.1 を応答し、しかも KeepAlive On なら KeepAlive も有効になります。そのため KeepAliveTimeout で指定された秒数まで接続しっぱなしになります。

Apache Bench はサーバからのアクティブクローズを待つので KeepAliveTimeout の秒数が経過するまでリクエストが終わらなくなります。

Docker Hub の php:apache のイメージは KeepAlive ONKeepAliveTimeout 5 です。

curl だと発生しない

curl で HTTP/1.0 を指定してもこの現象は発生しません。

time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK
Date: Sun, 30 Jul 2017 08:54:12 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/7.1.7
Content-Length: 0
Content-Type: text/html; charset=UTF-8


real    0m0.012s
user    0m0.003s
sys     0m0.004s

curl はリクエストを 1.0 で送ってもレスポンスが 1.1 なら 1.1 のクライアントとして振る舞うからだと思います。ので、Content-Length を元にレスポンスの終わりを判断できます。

なお、php のコードを次のようにすると、

<?php
header('HTTP/1.1 200 OK');
flush();

curl の応答は次のようになります。

time curl -i -0 http://localhost:8888/
HTTP/1.1 200 OK
Date: Sun, 30 Jul 2017 08:57:05 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/7.1.7
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

real    0m0.014s
user    0m0.002s
sys     0m0.005s

この場合は Transfer-Encoding: chunked なので、チャンクサイズ 0 でレスポンスの終端が判断できます。

zend-expressive

この現象、zend-expressive を Docker Hub の php:apache で動かして ab したときのスコアがおかしなことになって気づきました。

zend-expressive で使われている zend-diactoros のレスポンスオブジェクトは普通に作るとリクエストの内容に依らず 1.1 で固定のためです。

パイプラインの最初のほうでリクエストのバージョンを元にレスポンスのバージョンを変更するミドルウェアを登録しておけば解決します。

// pipeline.php

use Psr\Http\Message\ServerRequestInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;

$app->pipe(function (ServerRequestInterface $request, DelegateInterface $delegate) {
    return $delegate->process($request)->withProtocolVersion($request->getProtocolVersion());
});

nginx+php-fpm では発生しない

ちなみに nginx+php-fpm の構成では発生しませんでした。

nginx の場合は php-fpm が 1.1 を返してきたとしても、リクエストのバージョンが 1.0 なら Connection: close を付与したうえでサーバからアクティブクローズするようです。

結局どこが悪いの?

現代の PHP は http_response_code があるのでレスポンスコードを指定するためだけにプロトコルバージョンまで付けて header 関数を使うべきでは無いと思いますけれども、zend-diactoros は header 関数でステータスコードを指定しています。ただ、PSR-7 の ResponseInterfacegetProtocolVersion/withProtocolVersion を持っているので、レスポンスの送信時にプロトコルバージョンも指定するのは至って自然な気がします。

なので、zend-expressive でレスポンスのプロトコルバージョンがリクエストのプロトコルバージョンに基いていないのがダメなのだろうか・・・?

でも、HTTP/1.0 のリクエストに対して HTTP/1.1 のレスポンスを返すこと自体は合法なようです。

ただ、サーバが「おれ HTTP/1.1 でも大丈夫だよ」とクライアントに伝えるためであって、KeepAlive などの HTTP/1.1 の機能は有効にするべきではない、気がする、ので、Apache が悪いような気もする。

あるいは zend-expressive が(あるいは mod_php が) HTTP/1.0 なリクエストに対しては Connection :close でレスポンスを返すべき?