プライベートISUCONのISHOCON2のresponder版を実装してみた

### はじめに

最近ちょっと話題になった[responder](https://github.com/kennethreitz/responder)を勉強するため、scoutyのCTOが趣味でつくっているプライベートISUCONの課題である[ISHOCON2](https://github.com/showwin/ISHOCON2)のPython実装をresponderで書き換えてみました。

### responder

[responder](https://github.com/kennethreitz/responder)はかの有名な[requests](http://docs.python-requests.org/en/master/)の作者が開発しているWEBアプリケーションフレームワークです。DjangoよりはFlaskのような軽量フレームワークに分類されるかと思いますが、後発だけあってインターフェースなどが整っているようです。

実際の実装などは`uvicorn`や`starlette`を使っているようです。

ちなみに何も考えずに`pip install responder`すると起動時に

```
ModuleNotFoundError: No module named 'starlette.lifespan'
```

が出て死にます。

https://github.com/kennethreitz/responder/issues/255

にあるように

```
pip install starlette==0.8
```

でstarletteの0.8を明示しないといけないようです。

### ISHOCON

@showwinさんがつくっているプライベートのISUCONです。1人で８時間程度で課題をこなせる程度になっているので本家に比べてボリュームは少なめになっています。しかし、スタンダードなWEBアプリケーションに必要な要素を含んでいます。


### responderでのあれこれ

ISHOCONをresponderで書き換えていった上で、いくつかresponderのポイントをまとめていきます。

#### Databaseとの接続

responderはイベントループで動く非同期アプリケーションなので、DBとの接続を初期化するタイミングに注意します。

[ここ](https://python-responder.org/en/latest/api.html#responder.API.on_event)をみると、`on_event`を使ってサーバの起動停止時のタイミングで初期化と破棄をするようにするのが良いようです。ちなみに、ドキュメントでは`cleanup`というイベントでしたが、実際には`shutdown`を指定するようです。

```python
import responder

api = responder.API()


@api.on_event('startup')
async def open_database_connection_pool():
    print('startup')


@api.on_event('shutdown')
async def open_database_connection_pool():
    print('shutdown')

if __name__ == '__main__':
    api.run(address='0.0.0.0', port=8080, debug=True)
```

これを実行するとこんな感じで起動停止時にメッセージが出ます。

```
$ python app.py
INFO: Started server process [1460]
INFO: Waiting for application startup.
startup  ★
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
^CINFO: Stopping server process [1460]
INFO: Waiting for application shutdown.
shutdown  ★
```

あとは、ここにこんな感じで`aiomysql`のコードを差し込めばよいわけです。

```python

@api.on_event('startup')
async def open_database_connection_pool():
    pool = await aiomysql.create_pool(**{
        :<DB接続パラメータ>
        })
    api.mysql = pool


@api.on_event('shutdown')
async def open_database_connection_pool():
    api.mysql.close()
    await api.mysql.wait_closed()

```

使うときは以下のようにします。

```python

@api.route('/')
async def index(req, resp):
    async with api.mysql.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute('select version() as version')
            result = await cur.fetchone()
            resp.text = result['version']
```

これで`/`にアクセスすると使っているMySQLのバージョンが表示されます。


#### テンプレートエンジン

少し前に話題になったSanicとかだとテンプレートエンジンバンドルされてなかったりしましたが、responderはデフォルトでJinja2が入ってるので簡単です。`api.template`でそのままJinja2の`render`が呼ばれるので結果を`resp.content`に詰めるだけです。

```python
    resp.content = api.template(
        'index.html',
        candidates=candidates,
        parties=parties,
        sex_ratio=sex_ratio
    )
```

テンプレートファイルの格納先などは、`responder.API`のコンストラクタで変更できるようになっています。

```python
api = responder.API(
    templates_dir='templates' # 省略時はデフォルトでtemplatesが入るので明示しなくてもよい
)
```

#### 静的ファイル配信

実際の運用や、チューンしたISUCONの課題でも静的ファイルはnginxなどで返すことにはなりますが、初期実装はたいていアプリケーションサーバがCSSやJSなどの静的ファイルを配信しているので、responderで静的ファイルを配信するようにします。

responderでは[whitenoise](http://whitenoise.evans.io/en/stable/)という静的ファイル配信用のライブラリが組み込まれています。デフォルトでは`/static`というパスでのアクセスを、`static`ディレクトリに割り当てて静的ファイルの配信が行われます。これを変更するには`responder.API`のコンストラクタで変更できるようになっています。

```python
api = responder.API(
    :
    static_dir='public',
    static_route='/public',
)
```

この設定で、`/public/hoge.css`というアクセスは`<APP_ROUTE>/public/hoge.css`というファイルを戻すようになります。

#### リダイレクト

意外とドキュメントみてもよくわからなかったのがリダイレクトの実装方法。

```python
api.redirect(
    resp=resp,
    location='/'
)
```

これを実行すると、`/`にリダイレクトされます。実装されているソースはこれでした。

```python:responder.api.API#redirect
    def redirect(
        self, resp, location, *, set_text=True, status_code=status_codes.HTTP_301
    ):
        """Redirects a given response to a given location.

        :param resp: The Response to mutate.
        :param location: The location of the redirect.
        :param set_text: If ``True``, sets the Redirect body content automatically.
        :param status_code: an `API.status_codes` attribute, or an integer, representing the HTTP status code of the redirect.
        """

        # assert resp.status_code.is_300(status_code)

        resp.status_code = status_code
        if set_text:
            resp.text = f"Redirecting to: {location}"
        resp.headers.update({"Location": location})
```

ステータスコードいれて、ヘッダーを更新しているだけなので自分でやってもまぁ同じですね。

#### GET/POSTでのルーティング

Flaskとかだと

```python

@app.route('/vote', methods=['GET'])
def vote_get(request):
   :


@app.route('/vote', methods=['POST'])
def vote_get(request):
   :

```

みたいなことができます。responderの場合、これに相当する機能は**多分なさそう??** (少なくとも`api.route`では相当する引数を取るようにはできてなかった)

そのため、こんな感じで書くしかなさそうです・・？

```python
@api.route('/vote')
async def vote(req, resp):
   if req.method == 'post':
      await vote_post(req, resp)
   else:
      await vote_get(req, resp)
```

#### formデータの取得

フォームからPOSTされたデータを取得する場合は、以下のように`req.media()`を使用します。

```python
async def post_vote(req, resp):
    form = await req.media()
```

`await`する必要がある点に注意します。戻されるのは`responder.models.QueryDict`というDictライクなオブジェクトなのでそのまま使うことができます。なお、`req.media`は以下のようなコードになっています。

```python:responder.models.Request#media
    async def media(self, format=None):
        """Renders incoming json/yaml/form data as Python objects. Must be awaited.

        :param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
        """

        if format is None:
            format = "yaml" if "yaml" in self.mimetype or "" else "json"
            format = "form" if "form" in self.mimetype or "" else format

        if format in self.formats:
            return await self.formats[format](self)
        else:
            return await format(self)
```

見ればわかるように、`memetype`に応じてフォーマッタが切り替わっていますので、`form`以外のmimeの場合は異なる形式で戻されるようです。

#### マルチバイトを含んだURLマッピング

例えば以下のようなroutingの設定があります。

```python
@api.route('/political_parties/{name}')
async def political_parties(req, resp, name):
    print(name)
```

この状態で`/political_parties/国民元気党`というURLでアクセスすると以下の結果になります。

```
%e5%9b%bd%e6%b0%91%e5%85%83%e6%b0%97%e5%85%9a
```

URLエンコードされた状態でそのまま渡されてしまっています。これをresponder側の設定などでどうにかできないか探したのですが、ちょっとなさそうでしたので`urllib.parse.unquote_plus`で自前でアンエスケープするしかなさそうです。

### 完成コード

https://github.com/denzow/ISHOCON2/tree/responder-impl/webapp/python_responder

にresponderで移植した完成版がおいてありますので興味があればご覧ください。import文周りが他の実装とくらべてかなりスッキリしました。

### パフォーマンス

最後にほかの実装との比較をしておきます。現在ISHOCON2にはPythonの実装として`Flask`と`Sanic`のものがありますので今回の`responder`を含めた３種で比較します。スコアは、ISHOCON2に同梱されているベンチマークを以下のコマンドで実行した際の結果です。

```
# ./benchmark --ip app
```

|実装|スコア|
|---|----|
|Flask|2120|
|Sanic|8068|
|responder|6542|

Flaskはやっぱりasyncioを使っているSanic/responderに比べると遅いです。そしてSanicの速さが目立つ結果になりました。今回のresponderのコードはほぼSanic版の移植でしたので、ISUCON用途でresponderを採用することはなさそうです。(この記事の存在意義が薄れる)

### 感想

スタンダードなWEBアプリケーションを作る上では、responderは必ずしもわかりやすいものではないような気持ちになりました。FlaskやSanicとくらべると少し生のレスポンスやリクエストをいじっている気分です。

しかしresponderは非同期タスクやWebsocketの実装、GraphQL機能など、今回出てこなかった機能をかなり持っていますのでそのような用途であればきっと便利なんだろうと思います。