web-dev-qa-db-ja.com

Flaskが応答を返した後に関数を実行します

実行する必要のあるコードがありますafterFlaskは応答を返します。 Celeryのようなタスクキューを設定するほど複雑ではないと思います。重要な要件は、Flaskがこの関数を実行する前にクライアントに応答を返す必要があることです。関数が実行されるのを待つことはできません。

これに関するいくつかの既存の質問がありますが、回答がクライアントに送信された後、タスクの実行に対処する回答はありません。それらは引き続き同期的に実行され、その後、応答が返されます。

23
Brandon Wang

要するに、Flaskはこれを達成するための特別な機能を提供しないということです。単純な1回限りのタスクについては、以下に示すPythonのマルチスレッドを検討してください。より複雑な構成の場合は、RQやCeleryなどのタスクキューを使用します。

どうして?

関数Flaskが提供する機能と、それらがdo notで目的を達成する理由を理解することが重要です。これらはすべて他の場合に有用であり、読みやすいものですが、バックグラウンドタスクには役立ちません。

Flaskのafter_requestハンドラー

Flaskのafter_requestハンドラーは、 遅延リクエストコールバックのこのパターン および リクエストごとに異なる関数をアタッチするスニペット で詳しく説明されているように、コールバック関数にリクエストを渡します。意図されたユースケースは、Cookieを添付するなど、リクエストをmodifyすることです。

したがって、リクエスト自体は結果として変更されることが予想されるため、これらのハンドラの実行が完了するまでリクエストは待機します。

Flaskのteardown_requestハンドラー

これはafter_requestに似ていますが、teardown_requestrequestオブジェクトを受け取りません。つまり、リクエストを待つことはありませんよね?

同様のStack Overflowの質問に対するこの答え が示唆するように、これは解決策のようです。また、Flaskのドキュメントでは、 ティアダウンコールバックは実際のリクエストとは独立している であり、リクエストコンテキストを受け取らないと説明されているため、これを信じる正当な理由があります。

残念ながら、teardown_requestはまだ同期的です。Flaskのリクエスト処理の後半で、リクエストが変更できなくなったときに発生します。 Flaskは、ティアダウン関数の完了を待機しますは応答を返す前に完了します、 このFlaskコールバックとエラーのリスト

Flaskのストリーミング応答

Flaskは、ジェネレータをResponse()に渡すことで応答をストリーミングできます。 同様の質問に対するこのスタックオーバーフローの回答 が示唆しているように。

ストリーミングの場合、クライアントはdoesでリクエストが完了する前に応答の受信を開始します。ただし、リクエストは引き続き同期的に実行されるため、リクエストを処理するワーカーはストリームが終了するまでビジーです。

このFlaskストリーミングのパターン には、stream_with_context()の使用に関するドキュメントが含まれています。これは、リクエストコンテキストを含めるために必要です。

それで解決策は何ですか?

Flaskは、Flaskの責任ではないため、関数をバックグラウンドで実行するソリューションを提供しません。

ほとんどの場合、この問題を解決する最良の方法は、RQやCeleryなどのタスクキューを使用することです。これらは、設定、スケジューリング、ワーカーの配布などのトリッキーなものを管理します。これは、このタイプの質問に対する最も一般的な回答であり、最も正確であり、コンテキストなどを考慮した方法で設定することを強制します。正しく。

バックグラウンドで関数を実行する必要があり、これを管理するキューを設定したくない場合は、Pythonの組み込みの threading または multiprocessing を使用してバックグラウンドワーカーを生成できます。 。

バックグラウンドタスクからrequestまたはFlaskのスレッドローカルにアクセスすることはできません。リクエストはそこでアクティブにならないためです。代わりに、作成時に必要なデータをビューからバックグラウンドスレッドに渡します。

@app.route('/start_task')
def start_task():
    def do_work(value):
        # do something that takes a long time
        import time
        time.sleep(value)

    thread = Thread(target=do_work, kwargs={'value': request.args.get('value', 20))
    thread.start()
    return 'started'
33
Brandon Wang

Flaskは WSGIアプリ であり、その結果、応答後は基本的に何も処理できません。これがそのようなハンドラーが存在しない理由です。WSGIアプリ自体は、WSGIサーバーへの応答イテレーターオブジェクトの構築のみを担当します。

WSGIサーバー (ただし gunicorn など)はこの機能を非常に簡単に提供できますが、アプリケーションをサーバーに結び付けることはいくつかの理由で非常に悪い考えです。

この正確な理由から、WSGIは Middleware の仕様を提供し、Werkzeugは一般的なミドルウェア機能を簡素化するための多くのヘルパーを提供します。その中には ClosingIterator クラスがあり、これにより、リクエストが閉じられた後に実行される応答イテレータのcloseメソッドにメソッドをフックできます。

Flask拡張として行われた素朴なafter_response実装の例を次に示します。

import traceback
from werkzeug.wsgi import ClosingIterator

class AfterResponse:
    def __init__(self, app=None):
        self.callbacks = []
        if app:
            self.init_app(app)

    def __call__(self, callback):
        self.callbacks.append(callback)
        return callback

    def init_app(self, app):
        # install extension
        app.after_response = self

        # install middleware
        app.wsgi_app = AfterResponseMiddleware(app.wsgi_app, self)

    def flush(self):
        for fn in self.callbacks:
            try:
                fn()
            except Exception:
                traceback.print_exc()

class AfterResponseMiddleware:
    def __init__(self, application, after_response_ext):
        self.application = application
        self.after_response_ext = after_response_ext

    def __call__(self, environ, after_response):
        iterator = self.application(environ, after_response)
        try:
            return ClosingIterator(iterator, [self.after_response_ext.flush])
        except Exception:
            traceback.print_exc()
            return iterator

この拡張機能は次のように使用できます。

import flask
app = flask.Flask("after_response")
AfterResponse(app)

@app.after_response
def say_hi():
    print("hi")

@app.route("/")
def home():
    return "Success!\n"

「/」をカールすると、ログに次のように表示されます。

127.0.0.1 - - [24/Jun/2018 19:30:48] "GET / HTTP/1.1" 200 -
hi

これにより、スレッド(GIL ??)を導入したり、タスクキューとクライアントソフトウェアをインストールおよび管理したりすることなく、単純に問題を解決できます。

14
Matthew Story

Flaskブループリントのミドルウェアソリューション

これは、Matthew Storyによって提案されたものと同じソリューションです(これは私見-Matthewに感謝)。Flaskブループリントに適合しています。ここでの秘密は、current_appプロキシを使用してアプリのコンテキストを取得することです。詳細については here を参照してください( http://flask.pocoo.org/docs/1.0/appcontext/

AfterThisResponseおよびAfterThisResponseMiddlewareクラスが.utils.after_this_response.pyのモジュールに配置されていると仮定しましょう

次に、Flaskオブジェクトの作成が行われる場所で、たとえば...

__init__.py

from api.routes import my_blueprint
from .utils.after_this_response import AfterThisResponse

app = Flask( __name__ )
AfterThisResponse( app )
app.register_blueprint( my_blueprint.mod )

そして、ブループリントモジュールで...

a_blueprint.py

from flask import Blueprint, current_app

mod = Blueprint( 'a_blueprint', __name__, url_prefix=URL_PREFIX )

@mod.route( "/some_resource", methods=['GET', 'POST'] )
def some_resource():
    # do some stuff here if you want

    @current_app.after_this_response
    def post_process():
        # this will occur after you finish processing the route & return (below):
        time.sleep(2)
        print("after_response")

    # do more stuff here if you like & then return like so:
    return "Success!\n"
0
Paul Brackin

あなたは私がそれを試したこのコードを使用することができます。それは動作します。

このコードは、文字列「message」を出力します。スケジューリング時間から3秒後。必要に応じて時間を変更できます。

import time, traceback
import threading

def every(delay,message, task):
  next_time = time.time() + delay
  time.sleep(max(0, next_time - time.time()))
  task(message)

def foo(message):
  print(message+"  :foo", time.time())



def main(message):
    threading.Thread(target=lambda: every(3,message, foo)).start()

main("message")
0
Muhammad Usman