Flask'ta eşzamansız bir görev yapmak


104

Flask'ta WSGIeşzamanlı ve engelleyici olması dışında gerçekten iyi çalışan bir uygulama yazıyorum . Özellikle üçüncü taraf bir API'ye çağrı yapan bir görevim var ve bu görevin tamamlanması birkaç dakika sürebilir. Bu aramayı yapmak (aslında bir dizi arama) ve çalışmasına izin vermek istiyorum. kontrol Flask'a geri dönerken.

Benim görüşüm şuna benziyor:

@app.route('/render/<id>', methods=['POST'])
def render_script(id=None):
    ...
    data = json.loads(request.data)
    text_list = data.get('text_list')
    final_file = audio_class.render_audio(data=text_list)
    # do stuff
    return Response(
        mimetype='application/json',
        status=200
    )

Şimdi, yapmak istediğim çizgiye sahip olmak

final_file = audio_class.render_audio()

Flask istekleri işlemeye devam ederken, yöntem döndüğünde çalıştırılacak bir geri arama sağlar. Bu, Flask'ın eşzamansız olarak çalışması için ihtiyacım olan tek görev ve bunu en iyi nasıl uygulayacağım konusunda bazı tavsiyeler almak istiyorum.

Twisted ve Klein'a baktım, ancak Threading yeterli olacağından, aşırı olduklarından emin değilim. Veya Kereviz bunun için iyi bir seçimdir?


Bunun için genellikle kereviz kullanırım ... aşırı olabilir ama afaik diş açma web ortamlarında iyi çalışmıyor (iirc ...)
Joran Beasley

Sağ. Evet - Ben sadece Kereviz'i araştırıyordum. İyi bir yaklaşım olabilir. Flask ile uygulaması kolay mı?
Darwin Tech

heh ben de bir soket sunucusu kullanma eğilimindeyim (flask-socketio) ve evet bunun oldukça kolay olduğunu düşündüm ... en zor kısım her şeyi kurmaktı
Joran Beasley

5
Ben kontrol öneriyoruz bu out. Bu adam genel olarak flask için harika öğreticiler yazıyor ve bu, asenkron görevleri bir flask uygulamasına nasıl entegre edeceğinizi anlamak için harika.
atlspin

Yanıtlar:


106

Sizin için asenkron görevi yerine getirmek için Kerevizi kullanırdım. Görev kuyruğunuz olarak hizmet vermesi için bir aracı kurmanız gerekir (RabbitMQ ve Redis önerilir).

app.py:

from flask import Flask
from celery import Celery

broker_url = 'amqp://guest@localhost'          # Broker URL for RabbitMQ task queue

app = Flask(__name__)    
celery = Celery(app.name, broker=broker_url)
celery.config_from_object('celeryconfig')      # Your celery configurations in a celeryconfig.py

@celery.task(bind=True)
def some_long_task(self, x, y):
    # Do some long task
    ...

@app.route('/render/<id>', methods=['POST'])
def render_script(id=None):
    ...
    data = json.loads(request.data)
    text_list = data.get('text_list')
    final_file = audio_class.render_audio(data=text_list)
    some_long_task.delay(x, y)                 # Call your async task and pass whatever necessary variables
    return Response(
        mimetype='application/json',
        status=200
    )

Flask uygulamanızı çalıştırın ve kereviz işçinizi çalıştırmak için başka bir işlem başlatın.

$ celery worker -A app.celery --loglevel=debug

Ben de Miguel Gringberg en başvururuz yazma yukarı Flask ile kereviz kullanarak derinlik kılavuzunda bir daha.


Kereviz sağlam bir çözümdür, ancak hafif bir çözüm değildir ve kurulması biraz zaman alır.
wobbily_col

34

Diş çekme başka bir olası çözümdür. Kereviz tabanlı çözüm, ölçekli uygulamalar için daha iyi olsa da, söz konusu uç noktada çok fazla trafik beklemiyorsanız, iş parçacığı uygun bir alternatiftir.

Bu çözüm, Miguel Grinberg'in PyCon 2016 Flask at Scale sunumuna dayanmaktadır , özellikle slayt sunumundaki 41 numaralı slayt. Onun kodu da github geçerli orijinal kaynağa ilgilenen kişiler için.

Kullanıcı açısından bakıldığında kod şu şekilde çalışır:

  1. Uzun süre çalışan görevi gerçekleştiren uç noktaya bir çağrı yaparsınız.
  2. Bu uç nokta, görev durumunu kontrol etmek için bir bağlantıyla birlikte 202 Kabul Edildi değerini döndürür.
  3. Durum bağlantısına yapılan çağrılar, taks hala çalışırken 202'yi, görev tamamlandığında ise 200'ü (ve sonucu) döndürür.

Bir api çağrısını arka plan görevine dönüştürmek için, @async_api dekoratörünü eklemeniz yeterlidir.

İşte tam kapsamlı bir örnek:

from flask import Flask, g, abort, current_app, request, url_for
from werkzeug.exceptions import HTTPException, InternalServerError
from flask_restful import Resource, Api
from datetime import datetime
from functools import wraps
import threading
import time
import uuid

tasks = {}

app = Flask(__name__)
api = Api(app)


@app.before_first_request
def before_first_request():
    """Start a background thread that cleans up old tasks."""
    def clean_old_tasks():
        """
        This function cleans up old tasks from our in-memory data structure.
        """
        global tasks
        while True:
            # Only keep tasks that are running or that finished less than 5
            # minutes ago.
            five_min_ago = datetime.timestamp(datetime.utcnow()) - 5 * 60
            tasks = {task_id: task for task_id, task in tasks.items()
                     if 'completion_timestamp' not in task or task['completion_timestamp'] > five_min_ago}
            time.sleep(60)

    if not current_app.config['TESTING']:
        thread = threading.Thread(target=clean_old_tasks)
        thread.start()


def async_api(wrapped_function):
    @wraps(wrapped_function)
    def new_function(*args, **kwargs):
        def task_call(flask_app, environ):
            # Create a request context similar to that of the original request
            # so that the task can have access to flask.g, flask.request, etc.
            with flask_app.request_context(environ):
                try:
                    tasks[task_id]['return_value'] = wrapped_function(*args, **kwargs)
                except HTTPException as e:
                    tasks[task_id]['return_value'] = current_app.handle_http_exception(e)
                except Exception as e:
                    # The function raised an exception, so we set a 500 error
                    tasks[task_id]['return_value'] = InternalServerError()
                    if current_app.debug:
                        # We want to find out if something happened so reraise
                        raise
                finally:
                    # We record the time of the response, to help in garbage
                    # collecting old tasks
                    tasks[task_id]['completion_timestamp'] = datetime.timestamp(datetime.utcnow())

                    # close the database session (if any)

        # Assign an id to the asynchronous task
        task_id = uuid.uuid4().hex

        # Record the task, and then launch it
        tasks[task_id] = {'task_thread': threading.Thread(
            target=task_call, args=(current_app._get_current_object(),
                               request.environ))}
        tasks[task_id]['task_thread'].start()

        # Return a 202 response, with a link that the client can use to
        # obtain task status
        print(url_for('gettaskstatus', task_id=task_id))
        return 'accepted', 202, {'Location': url_for('gettaskstatus', task_id=task_id)}
    return new_function


class GetTaskStatus(Resource):
    def get(self, task_id):
        """
        Return status about an asynchronous task. If this request returns a 202
        status code, it means that task hasn't finished yet. Else, the response
        from the task is returned.
        """
        task = tasks.get(task_id)
        if task is None:
            abort(404)
        if 'return_value' not in task:
            return '', 202, {'Location': url_for('gettaskstatus', task_id=task_id)}
        return task['return_value']


class CatchAll(Resource):
    @async_api
    def get(self, path=''):
        # perform some intensive processing
        print("starting processing task, path: '%s'" % path)
        time.sleep(10)
        print("completed processing task, path: '%s'" % path)
        return f'The answer is: {path}'


api.add_resource(CatchAll, '/<path:path>', '/')
api.add_resource(GetTaskStatus, '/status/<task_id>')


if __name__ == '__main__':
    app.run(debug=True)


Bu kodu kullandığımda, werkzeug.routing.BuildError hatası alıyorum: "gettaskstatus" uç noktası için url değeri ['task_id'] ile oluşturulamıyor Bir şey mi eksik?
Nicolas Dufaur

15

Ayrıca, multiprocessing.Processile kullanmayı deneyebilirsiniz daemon=True; process.start()yöntem engellemez ve arka planda pahalı işlev yürütür iken arayana hemen yanıt / durumunu dönebilirsiniz.

Falcon çerçevesiyle çalışırken benzer bir sorun yaşadım ve daemonsüreci kullanmak yardımcı oldu.

Aşağıdakileri yapmanız gerekir:

from multiprocessing import Process

@app.route('/render/<id>', methods=['POST'])
def render_script(id=None):
    ...
    heavy_process = Process(  # Create a daemonic process with heavy "my_func"
        target=my_func,
        daemon=True
    )
    heavy_process.start()
    return Response(
        mimetype='application/json',
        status=200
    )

# Define some heavy function
def my_func():
    time.sleep(10)
    print("Process finished")

Hemen bir yanıt almalısınız ve 10 saniye sonra konsolda yazılı bir mesaj görmelisiniz.

NOT: daemonicİşlemlerin herhangi bir alt süreç oluşturmasına izin verilmediğini unutmayın .


zaman uyumsuz, ne iş parçacığı ne de çoklu işlem yapmayan belirli bir eşzamanlılık türüdür. Ancak iş parçacığı, eşzamansız görev olarak amaç olarak çok daha yakın,
tortal

5
Demek istediğini anlamıyorum. Yazar, "arka planda" çalışan bir görev olan asenkron bir görevden bahsediyor, böylece arayan kişi bir yanıt alana kadar engellemez. Bir deamon işleminin üretilmesi, bu tür bir eşzamansızlığın elde edilebileceği yerlerin bir örneğidir.
Tomasz Bartkowiak

ya /render/<id>uç nokta sonucunda bir şey beklerse my_func()?
Will Gu

Sen yapabilir my_funcörneğin diğer bazı bitiş noktasına gönderme yanıt / kalp atışlarını. Ya da iletişim kurabileceğiniz bazı mesaj kuyrukları oluşturabilir ve paylaşabilirsinizmy_func
Tomasz Bartkowiak
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.