Twiggy で応答の遅いサーバーを模倣する

数年前から Advent Calendar に憧れがあったんですが，今年はライバル？が少なくて初投稿できてひゃっほーい，な @dayflower です。



クライアントの性能テスト用に，単純な応答をゆっくり返すモックサーバをささっと書く必要に駆られたことはありませんか。わたしはあります。

(単純な) レスポンスを，だらだらと返したい！でも大量にリクエストをさばきたい！できれば単純に書きたい！

`AnyEvent` ベースの PSGI compliant なウェブサーバの `Twiggy` を使えば，応答を非同期に返すことができるので，低コスト低リソースでそんなことができます。

## おことわり

- おそくておそくて connection すら張れないよ，という状況は模倣できません。
    - `Twiggy` (というか `AnyEvent::Socket`) が全力で accept するので。
    - ふつうに event model でないサーバアプリたててください。
    - 503 を模倣するとかなら (もちろん) できます。
- 1バイト送るのに1秒かかるの！，という状況は模倣していません。
    - 実のところ `Twiggy` (&amp; `AnyEvent`) の得意分野なので，やってできないことはありません。responder にとりあえず empty body 返して，チマチマ writer で書き込めばいいのです。

## まずはシンプルに

単純なレスポンスを，リクエストからちょい遅れてから返すサーバをつくります。

とりあえず， `cpanfile` から。

```pl
requires "Twiggy";
requires "AnyEvent";
```

`AnyEvent` は `Twiggy` が依存しているので書かなくてもいいです。なんとなく書きました。

次にアプリケーション本体です。

```pl
use strict;
use warnings;
use AnyEvent;
use Plack::Loader;

my $app = sub {
    my $env = shift;

    if ($env->{REQUEST_URI} =~ m{(?: ^ | / ) favicon.ico $}x) {
        return res_404();
    }

    warn "request: " . $env->{REQUEST_URI};

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer(5, 0, sub {
            my $content = '200 OK';
            my %headers = (
                'Content-Type'   => 'text/plain',
                'Content-Length' => length $content,
            );

            $responder->([ 200, [ %headers ], [ $content ] ]);

            undef $w;
        });
    };
};

sub res_404 {
    return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);
```

そのまま `carton exec server.pl` などとすると動きます。

とても単純だったので拍子抜けしたのではないでしょうか。

いくつかポイントがあります。

- ふつうの PSGI App のように，リクエストに対してささっと返すこともできます。
    - favicon への応答に関するところです (サブルーチン化する必要はないのですが)
        - favicon のログがでるとうざかったりするので，除外しています。
- 非同期に応答するには，ふつうの PSGI handler の戻り値 (配列) ではなく，サブルーチンリファレンスを返します。そのサブルーチンに第一引数として，応答用のサブルーチンリファレンス (上記サンプルでの `$responder` ) が渡されるので，それを使ってレスポンスを返します。
    - 上記サンプルでは，さらに `AE::timer` (`AnyEvent->timer`) を利用して，5秒間応答を遅延させています。
- 本当はサーバが非同期に応答できるかどうか， `psgi.streaming` 環境変数を検査して判定する必要があります。
    - ですが今回は `Twiggy` 決め打ちなので，省略しています。
- という，いままで述べたあたりのことは [PSGI Spec の Delayed Response and Streaming Body 項](https://metacpan.org/pod/release/MIYAGAWA/PSGI-1.102/PSGI.pod#Delayed-Response-and-Streaming-Body) に全部書いてありますので，そちらを読むことをおすすめします。
- (`Twiggy` 決め打ちなので) `Plack::Loader` 使っていますが， `$app` をそのまま返せば，ふつうの PSGI ファイルになります。
    - ただし `psgi.streaming` をサポートした非同期応答に対応した PSGI サーバでないと動きません。

応答時間をランダムに変えたり，相手によって変えたり，もっとまともなレスポンスを返したり，夢がひろがりますね。

## 応用 (1) カラフルに出力する

クライアントの送信する内容を覗いてみたくなったことはありませんか。

```pl
use strict;
use warnings;
use Term::ANSIColor qw( colored );
use AnyEvent;
use Plack::Request;
use Plack::Loader;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);

    if ($req->path_info =~ m{(?: ^ | / ) favicon.ico $}x) {
        return res_404();
    }

    print "\n";

    # Request Method & URI
    my $meth_uri = sprintf '%s %s', $req->method, $req->uri->as_string;
    print colored([qw( cyan reverse )], $meth_uri), "\n";

    # Headers
    foreach my $field ($req->headers->header_field_names) {
        my $vals = $req->header($field);
        print colored([qw( yellow )], "$field: $vals"), "\n";
    }

    # Query Parameters
    my $query_params = $req->query_parameters;
    foreach my $field (sort keys %$query_params) {
        my $vals = join q{, }, $query_params->get_all($field);
        print colored([qw( green )], "$field: $vals"), "\n";
    }

    # TODO for Body Parameters!

    # Start to respond
    my $delay = 1 + int(rand(3));

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer($delay, 0, sub {
            my $content = '200 OK';
            my %headers = (
                'Content-Type'   => 'text/plain',
                'Content-Length' => length $content,
            );

            $responder->([ 200, [ %headers ], [ $content ] ]);

            undef $w;
        });
    };
};

sub res_404 {
    return [ 404, [ 'Content-Type', 'text/plain' ], [ '404 Not Found' ] ];
}

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 20080)->run($app);
```

これで

![](https://qiita-image-store.s3.amazonaws.com/0/5822/ac7a7dab-16e5-f7ae-c980-2678148442b3.png)
こんな感じで表示されるようになります。
(おまけで，遅延時間を乱数で決めるようにしています)

Request body の表示は実装していません。JSON のリクエストだったらパースして整形して表示したり，とかやってみてもいいですね。

## 応用 (2) イライラする Proxy を作る

せっかく非同期に応答できるので， `AnyEvent::HTTP` を使って，なんちゃってプロキシをつくってみようと思います。

```pl
use strict;
use warnings;
use AnyEvent;
use AnyEvent::HTTP;
use Plack::Loader;

my $app = sub {
    my $env = shift;

    warn $env->{REQUEST_URI};

    return sub {
        my $responder = shift;

        my $w; $w = AE::timer(
            3, 0,
            sub {
                http_request(
                    $env->{REQUEST_METHOD},
                    $env->{REQUEST_URI},
                    sub {
                        my ($body, $headers) = @_;

                        $responder->([ $headers->{Status}, [ %$headers ], [ $body ] ]);

                        undef $w;
                    }
                );
            }
        );
    };
};

Plack::Loader->load('Twiggy', host => '0.0.0.0', port => 8080)->run($app);
```

多段 callback になってしまって，見づらいですがご容赦を (どうすればいいんでしょう)。

本当はリクエストされた HTTP ヘッダの内容をプロキシ先に投げたりとかしないとちゃんと完全には動かないんですが，そのへんは宿題にしましょう。

ダイヤルアップ時代の気分をそこそこ感じられることうけあいです。

## 参考文献

- Twiggy 添付の websocket を利用したサンプルの [chat.psgi](https://metacpan.org/source/MIYAGAWA/Twiggy-0.1024/eg/chat-websocket/chat.psgi)
- [PSGI Spec の Delayed Response and Streaming Body 項](https://metacpan.org/pod/release/MIYAGAWA/PSGI-1.102/PSGI.pod#Delayed-Response-and-Streaming-Body)
- [lestrrat さんによる Perl Hackers Hub の AnyEvent の記事 in WEB+DB PRESS](http://gihyo.jp/dev/serial/01/perl-hackers-hub/000201)
    - [Perl 徹底攻略](http://www.amazon.co.jp/gp/product/477415864X/ref=as_li_ss_tl?ie=UTF8&camp=247&creative=7399&creativeASIN=477415864X&linkCode=as2&tag=dailydayflowe-22)にも収録されています。他にも Perl hacker にはお役立ちな記事も多くて，超おすすめです。

## おわりに

`Twiggy`, イベント駆動まわりのベースが `AnyEvent` なので，ほかの `AnyEvent` を使ったライブラリ (`AnyEvent::HTTP` とか `AnyEvent::IRC` とか) と組み合わせて使うにはもってこいだったりします。

さて，明日は lestrrat さんの『普通のデーモンを 1) Server::Starterでホットデプロイ+ 2) slow-restart対応にする』です！<del>……おや，書く人がまだ決まってない！書きたい人はチャンスです！</del>