この記事は全部俺 Advent Calendar 2018の4日目の記事です。
この記事 is 何?
Python + Flaskでバックグラウンド処理をしてレスポンスをすぐ返す方法について記載します。
※のっぴきならない理由がない限り、今から開発を始める場合はFlaskではなくresponderを使いましょう
この記事にresponderで同じことをする方法を記載しているのでよければご一読くださいm(_ _)m
tl;dr
- Flaskで非同期処理を実現する前に、Flask.run()は開発環境前提なので本番環境用にuWSGIを通す必要があります
- 処理を発火させた後、処理完了を待たずに終了することをfire and forgetと呼びます
- 新たにサーバを立てる場合で非同期処理する可能性がある場合はresponderを使いましょう(2度目)
筆者の環境
| software | version | 
|---|---|
| Ubuntu | 18.04.1 LTS | 
| Python | 3.6.5 | 
| Flask | 1.0.2 | 
Flaskでawait/asyncを使えるようにするまで
まずはHello, World
まず、FlaskでHello, Worldするコードを書いてみます。
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()は開発用なのでこれに対応して本番用の環境の作成から行っていきます。
 * 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]
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で新しく設定ファイルを作成し、以下のように記述します。
server {
  listen 80 default_server;
  server_name _;
  location / {
    include uwsgi_params;
    uwsgi_pass unix:///tmp/uwsgi.sock;
  }
}
ここでは、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を実行し、以下のようにファイルを書き換えます。
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を開始できずに発生するエラーです。
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をおすすめします。
responderで非同期処理を行うノウハウについても近い内にまとめたいと思います。
