Python + Flask + uWSGI + Nginxで処理完了を待たずに非同期処理(background処理)してレスポンスを先に返す方法

この記事は[全部俺 Advent Calendar 2018](https://adventar.org/calendars/3702)の4日目の記事です。

# この記事 is 何？
Python + Flaskでバックグラウンド処理をしてレスポンスをすぐ返す方法について記載します。
<font color="Red">※のっぴきならない理由がない限り、今から開発を始める場合はFlaskではなく[responder](https://python-responder.org/en/latest/#)を使いましょう</font>
[この記事](https://qiita.com/tez/items/939168dbb31905948f46)にresponderで同じことをする方法を記載しているのでよければご一読くださいm(_ _)m

## tl;dr
- Flaskで非同期処理を実現する前に、Flask.run()は開発環境前提なので本番環境用にuWSGIを通す必要があります
- 処理を発火させた後、処理完了を待たずに終了することを`fire and forget`と呼びます
- 新たにサーバを立てる場合で非同期処理する可能性がある場合は[responder](https://python-responder.org/en/latest/#)を使いましょう(2度目)

## 筆者の環境
|software |version    |
|:--------|----------:|
|Ubuntu   |18.04.1 LTS|
|Python   |3.6.5      |
|Flask    |1.0.2      |

# Flaskでawait/asyncを使えるようにするまで
## まずはHello, World
まず、Flaskで`Hello, World`するコードを書いてみます。

```~/app.py
import flask

app = flask.Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World\n'

if __name__ == '__main__':
    app.run(host='0.0.0.0')
```

そして`python3 app.py`でサーバ起動した後、`curl http://0.0.0.0:5000/`すると、`Hello, World`が表示されました。同様に、ブラウザから`http://[サーバIP]:5000`にアクセスしても`Hello, World`が表示されるはずです。

## Flaskのprocessesを増やしてみる
`app.run(host="0.0.0.0")`の部分を、`app.run(host="0.0.0.0", processes=10)`にして再度`python3 app.py`してみると、下記のエラーが出ます。
これは、Flask1.0.2のバグのようですが、そもそも3行目に`WARNING: Do not use the development server in a production environment.`と出ているように、`Flask.run()`は開発用なのでこれに対応して本番用の環境の作成から行っていきます。


```Python
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
Traceback (most recent call last):
  File "app.py", line 14, in <module>
    app.run(host="0.0.0.0", threaded=True, processes=10)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 943, in run
    run_simple(host, port, self, **options)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 814, in run_simple
    inner()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 774, in inner
    fd=fd)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 656, in make_server
    raise ValueError("cannot have a multithreaded and "
ValueError: cannot have a multithreaded and multi process server.
```

## 本番環境の作成
### uWSGIの準備
`sudo pip3 install uwsgi`でuwsgiをインストールした後、`vim ~/uwsgi.ini`で設定ファイルを作成し、以下のように記述します。

```~/uwsgi.ini
[uwsgi]
socket = /tmp/uwsgi.sock
chmod-socket = 666
module = app
master = true
processes = 2
```

この設定では、HTTPではなくUnixドメインソケットを作るようにしているので、無駄なポートを消費しません。
`processes = 2`が今回の肝で、これによってマルチプロセス処理を可能にしているので、後のasync/await処理にて非同期処理を実装することができます。

### Nginxの準備
`sudo apt install nginx`してnginxをインストールします。
`sudo vim /etc/nginx/conf.d/fireforget.conf`で新しく設定ファイルを作成し、以下のように記述します。

```/etc/nginx/conf.d/fireforget.conf
server {
  listen 80 default_server;
  server_name _;

  location / {
    include uwsgi_params;
    uwsgi_pass unix:///tmp/uwsgi.sock;
  }
}
```

ここでは、[uWSGIの準備](#uwsgiの準備)で設定したUnixドメインソケットをuwsgi_passで指定しているので、`/`へのアクセスをuWSGIに流すことが可能になっています。

### uWSGIとNginxの実行
```
sudo systemctl start nginx
sudo uwsgi --ini ~/uwsgi.ini
```

ここまで来たら、サーバ上で`curl http://127.0.0.1/`した場合に`Hello, World`が表示されるはずです。

## 並列処理化
`vim ~/app.py`を実行し、以下のようにファイルを書き換えます。

```~/app.py
import asyncio
import time
from datetime import datetime

import flask

app = flask.Flask(__name__)

@app.route('/')
def hello():
    # asyncioを使用し、処理を発火させた後完了を待たずにreturnする
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.ensure_future(sleep(10)))
    loop.close()

    print(f'[hello      ]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')
    return "Hello, World\n"

# 長い時間がかかる処理
async def sleep(seconds):
    with open
    print(f'[sleep start]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')
    time.sleep(seconds)
    print('slept!!')
    print(f'[sleep   end]{datetime.now().strftime("%Y/%m/%d %H:%M:%S")}')

if __name__ == '__main__':
    app.run()
```

これを実行すると、uWSGIから以下のように結果が出力されるはずです。
```
[hello      ]2018/12/04 14:45:43
```
ファイルの中身は以下のようになります。
```
[sleep start]2018/12/04 14:45:43
slept!!
[sleep   end]2018/12/04 14:45:53
```

以上でFlaskを用いた非同期実行が確認できました！

### 補足
ちなみに、上記のuWSGIとNginxを通さず、無理やり`python3 app.py`を実行してアクセスすると、以下のようなエラーログが出力されます。
これは、Flaskが非同期処理に対応したサーバではないのに無理やり`asyncio.get_event_loop()`を実行したために、新たなThreadを開始できずに発生するエラーです。

```Python
Traceback (most recent call last):
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/ubuntu/.local/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "app.py", line 11, in hello
    loop = asyncio.get_event_loop()
  File "/usr/lib/python3.6/asyncio/events.py", line 694, in get_event_loop
    return get_event_loop_policy().get_event_loop()
  File "/usr/lib/python3.6/asyncio/events.py", line 602, in get_event_loop
    % threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'Thread-1'.
127.0.0.1 - - [04/Dec/2018 13:47:07] "GET / HTTP/1.1" 500 -
```

## まとめ
どうでしたでしょうか？
Flask上で非同期処理をしたいだけなのに、かなり面倒な設定が多かったですね。。
繰り返しになりますが、今から開発を始める場合は[responder](https://python-responder.org/en/latest/#)をおすすめします。
[responder](https://python-responder.org/en/latest/#)で非同期処理を行うノウハウについても近い内にまとめたいと思います。