การสร้างงานแบบอะซิงโครนัสใน Flask


110

ฉันกำลังเขียนแอปพลิเคชันใน Flask ซึ่งใช้งานได้ดีจริงๆยกเว้นว่าWSGIเป็นแบบซิงโครนัสและบล็อก ฉันมีงานหนึ่งโดยเฉพาะซึ่งเรียกไปยัง API ของบุคคลที่สามและงานนั้นอาจใช้เวลาหลายนาทีจึงจะเสร็จสมบูรณ์ ฉันต้องการโทรออก (จริงๆแล้วเป็นการโทรหลายครั้ง) และปล่อยให้มันทำงาน ในขณะที่การควบคุมกลับไปที่ Flask

มุมมองของฉันดูเหมือนว่า:

@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
    )

ตอนนี้สิ่งที่อยากทำคือมีไลน์

final_file = audio_class.render_audio()

เรียกใช้และให้การเรียกกลับเพื่อดำเนินการเมื่อเมธอดส่งคืนในขณะที่ Flask สามารถดำเนินการตามคำขอต่อไปได้ นี่เป็นงานเดียวที่ฉันต้องการให้ Flask ทำงานแบบอะซิงโครนัสและฉันต้องการคำแนะนำเกี่ยวกับวิธีที่ดีที่สุดในการนำไปใช้

ฉันดู Twisted และ Klein แล้ว แต่ฉันไม่แน่ใจว่าพวกเขาโอเวอร์คิลเพราะ Threading อาจจะพอเพียง หรืออาจจะเป็นทางเลือกที่ดีสำหรับสิ่งนี้?


ฉันมักจะใช้คื่นฉ่ายสำหรับสิ่งนี้ ... มันอาจจะมากเกินไป แต่การทำเกลียว afaik ไม่ได้ผลดีในสภาพแวดล้อมของเว็บ (iirc ... )
Joran Beasley

ขวา. ใช่ - ฉันแค่ตรวจสอบคื่นฉ่าย อาจเป็นแนวทางที่ดี ใช้งานง่ายด้วย Flask?
Darwin Tech

เฮ้ฉันมักจะใช้เซิร์ฟเวอร์ซ็อกเก็ตด้วย (flask-socketio) และใช่ฉันคิดว่ามันค่อนข้างง่าย ... ส่วนที่ยากที่สุดคือการติดตั้งทุกอย่าง
Joran Beasley

6
ฉันจะแนะนำให้ตรวจสอบนี้ออก ผู้ชายคนนี้เขียนแบบฝึกหัดที่ยอดเยี่ยมสำหรับขวดโดยทั่วไปและสิ่งนี้เหมาะอย่างยิ่งสำหรับการทำความเข้าใจวิธีรวมงานอะซิงโครนัสเข้ากับแอปขวด
atlspin

คำตอบ:


110

ฉันจะใช้คื่นช่ายจัดการงานอะซิงโครนัสให้คุณ คุณจะต้องติดตั้งนายหน้าเพื่อใช้เป็นคิวงานของคุณ (แนะนำให้ใช้ RabbitMQ และ Redis)

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 ของคุณและเริ่มกระบวนการอื่นเพื่อเรียกใช้คนงานขึ้นฉ่ายของคุณ

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

ฉันยังจะอ้างถึงมิเกล Gringberg ของการเขียนขึ้นสำหรับข้อมูลเพิ่มเติมในคู่มือการใช้ความลึกให้กับคื่นฉ่ายกับขวด


คื่นช่ายเป็นวิธีแก้ปัญหาที่มั่นคง แต่ไม่ใช่วิธีแก้ปัญหาที่มีน้ำหนักเบาและใช้เวลาสักครู่ในการตั้ง
wobbily_col

36

การทำเกลียวเป็นอีกวิธีหนึ่งที่เป็นไปได้ แม้ว่าโซลูชันที่ใช้ Celery จะดีกว่าสำหรับการใช้งานในระดับ แต่หากคุณไม่คาดหวังว่าจะมีปริมาณการใช้งานมากเกินไปในจุดสิ้นสุดที่เป็นปัญหาการทำเกลียวก็เป็นทางเลือกที่ใช้ได้

โซลูชันนี้ใช้งานนำเสนอ PyCon 2016 Flask at Scale ของ Miguel Grinbergโดยเฉพาะสไลด์ 41ในสไลด์เดอร์ของเขา โค้ดของเขายังมีอยู่ใน githubสำหรับผู้ที่สนใจในต้นฉบับ

จากมุมมองของผู้ใช้รหัสจะทำงานดังนี้:

  1. คุณโทรไปยังปลายทางที่ทำงานที่รันเป็นเวลานาน
  2. จุดสิ้นสุดนี้ส่งคืน 202 Accepted พร้อมลิงก์สำหรับตรวจสอบสถานะงาน
  3. การเรียกไปยังลิงก์สถานะจะส่งคืน 202 ในขณะที่ taks ยังคงทำงานอยู่และส่งคืน 200 (และผลลัพธ์) เมื่องานเสร็จสมบูรณ์

ในการแปลงการเรียก API เป็นงานพื้นหลังให้เพิ่มมัณฑนากร @async_api

นี่คือตัวอย่างที่มีอยู่ครบถ้วน:

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)


เมื่อฉันใช้รหัสนี้ฉันมีข้อผิดพลาด werkzeug.routing.BuildError: ไม่สามารถสร้าง url สำหรับปลายทาง 'gettaskstatus' ด้วยค่า ['task_id'] ฉันขาดอะไรไปหรือเปล่า?
Nicolas Dufaur

17

คุณยังสามารถลองใช้multiprocessing.Processกับdaemon=True; process.start()วิธีจะไม่ปิดกั้นและคุณสามารถตอบกลับ / สถานะทันทีไปยังผู้โทรในขณะที่รันฟังก์ชั่นราคาแพงของคุณในพื้นหลัง

ฉันประสบปัญหาที่คล้ายกันขณะทำงานกับโครงร่างเหยี่ยวและใช้daemonกระบวนการช่วย

คุณจะต้องดำเนินการดังต่อไปนี้:

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")

คุณควรได้รับคำตอบทันทีและหลังจาก 10 วินาทีคุณจะเห็นข้อความที่พิมพ์ออกมาในคอนโซล

หมายเหตุ: โปรดทราบว่าdaemonicกระบวนการต่างๆไม่ได้รับอนุญาตให้สร้างกระบวนการย่อยใด ๆ


อะซิงโครนัสคือประเภทของการเกิดพร้อมกันบางประเภทซึ่งไม่ใช่ทั้งเธรดหรือการประมวลผลหลายขั้นตอน อย่างไรก็ตามการทำเธรดนั้นมีจุดประสงค์ที่ใกล้เคียงกว่ามากในฐานะงาน async
ทรมาน

5
ฉันไม่เข้าใจประเด็นของคุณ ผู้เขียนกำลังพูดถึงงานอะซิงโครนัสซึ่งเป็นงานที่รัน "อยู่เบื้องหลัง" เพื่อให้ผู้โทรไม่ปิดกั้นจนกว่าจะได้รับการตอบกลับ การวางไข่กระบวนการ deamon เป็นตัวอย่างของจุดที่สามารถทำได้แบบอะซิงโครไนซ์
Tomasz Bartkowiak

จะเกิดอะไรขึ้นถ้า/render/<id>จุดสิ้นสุดคาดว่าจะมีบางสิ่งเป็นผลมาจากmy_func()อะไร?
Will Gu

คุณสามารถmy_funcส่งการตอบสนอง / การเต้นของหัวใจไปยังจุดสิ้นสุดอื่น ๆ ได้เช่น หรือคุณสามารถสร้างและแบ่งปันคิวข้อความที่คุณสามารถสื่อสารด้วยmy_func
Tomasz Bartkowiak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.