Thực hiện một tác vụ không đồng bộ trong Flask


96

Tôi đang viết một ứng dụng trong Flask, ứng dụng này hoạt động thực sự tốt ngoại trừ tính năng WSGIđồng bộ và chặn. Tôi có một nhiệm vụ cụ thể gọi ra API của bên thứ ba và tác vụ đó có thể mất vài phút để hoàn thành. Tôi muốn thực hiện cuộc gọi đó (nó thực sự là một chuỗi các cuộc gọi) và để nó chạy. trong khi quyền điều khiển được trả lại cho Flask.

Chế độ xem của tôi trông giống như:

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

Bây giờ, những gì tôi muốn làm là có dòng

final_file = audio_class.render_audio()

chạy và cung cấp một lệnh gọi lại sẽ được thực thi khi phương thức trả về, trong khi Flask có thể tiếp tục xử lý các yêu cầu. Đây là tác vụ duy nhất mà tôi cần Flask chạy không đồng bộ và tôi muốn một số lời khuyên về cách tốt nhất để thực hiện việc này.

Tôi đã xem xét Twisted và Klein, nhưng tôi không chắc chúng quá mức cần thiết, vì có lẽ Threading là đủ. Hoặc có lẽ Cần tây là một lựa chọn tốt cho việc này?


Tôi thường sử dụng cần tây cho điều này ... nó có thể là quá mức cần thiết nhưng afaik việc luồng doesnt tốt trong môi trường web (iirc ...)
Joran Beasley

Đúng. Ừ - Tôi chỉ đang điều tra về Celery. Nó có thể là một cách tiếp cận tốt. Dễ thực hiện với Flask?
Darwin Tech

heh i xu hướng sử dụng một máy chủ socket cũng (bình-socketio) và yes i nghĩ rằng nó là khá dễ dàng ... phần khó nhất đã nhận được tất cả mọi thứ được cài đặt
Joran Beasley

4
Tôi muốn khuyên bạn nên kiểm tra điều này . Anh chàng này viết các hướng dẫn tuyệt vời cho flask nói chung và hướng dẫn này rất tuyệt để hiểu cách tích hợp các tác vụ không đồng bộ vào một ứng dụng flask.
atlspin

Câu trả lời:


100

Tôi sẽ sử dụng Celery để xử lý tác vụ không đồng bộ cho bạn. Bạn sẽ cần cài đặt một nhà môi giới để đóng vai trò là hàng đợi tác vụ của mình (nên dùng RabbitMQ và 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
    )

Chạy ứng dụng Flask của bạn và bắt đầu một quá trình khác để chạy công nhân cần tây của bạn.

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

Tôi cũng sẽ tham khảo bài viết của Miguel Gringberg để có hướng dẫn chuyên sâu hơn về cách sử dụng Cần tây với Flask.


34

Phân luồng là một giải pháp khả thi khác. Mặc dù giải pháp dựa trên Cần tây tốt hơn cho các ứng dụng ở quy mô lớn, nhưng nếu bạn không mong đợi quá nhiều lưu lượng truy cập trên điểm cuối được đề cập, phân luồng là một giải pháp thay thế khả thi.

Giải pháp này dựa trên bản trình bày PyCon 2016 Flask at Scale của Miguel Grinberg , cụ thể là slide 41 trong bản trình chiếu của anh ấy. của anh ấy cũng có sẵn trên github cho những người quan tâm đến nguồn gốc.

Từ góc độ người dùng, mã hoạt động như sau:

  1. Bạn thực hiện một cuộc gọi đến điểm cuối thực hiện tác vụ đang chạy dài.
  2. Điểm cuối này trả về 202 Được chấp nhận với một liên kết để kiểm tra trạng thái nhiệm vụ.
  3. Các lệnh gọi đến liên kết trạng thái trả về 202 trong khi các thẻ vẫn đang chạy và trả về 200 (và kết quả) khi tác vụ hoàn tất.

Để chuyển đổi một lệnh gọi api thành một tác vụ nền, chỉ cần thêm trình trang trí @async_api.

Dưới đây là một ví dụ đầy đủ:

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)

Khi tôi sử dụng mã này, tôi gặp lỗi werkzeug.routing.BuildError: Không thể tạo url cho điểm cuối 'gettaskstatus' với các giá trị ['task_id'] Tôi có thiếu thứ gì không?
Nicolas Dufaur

9

Bạn cũng có thể thử sử dụng multiprocessing.Processvới daemon=True; các process.start()phương pháp không chặn và bạn có thể trở lại một phản ứng / status ngay lập tức cho người gọi trong khi thực thi chức năng đắt tiền của bạn ở chế độ nền.

Tôi đã gặp sự cố tương tự khi làm việc với khung công tác falcondaemonquá trình sử dụng đã được giúp đỡ.

Bạn cần phải làm như sau:

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

Bạn sẽ nhận được phản hồi ngay lập tức và sau 10 giây, bạn sẽ thấy một thông báo được in trong bảng điều khiển.

LƯU Ý: Hãy nhớ rằng daemoniccác quy trình không được phép sinh ra bất kỳ quy trình con nào.


không đồng bộ là một loại đồng thời nhất định không phân luồng hay đa xử lý. Tuy nhiên, phân luồng gần với mục đích hơn nhiều như tác vụ không đồng bộ,
tra tấn

3
Tôi không hiểu quan điểm của bạn. Tác giả đang nói về một tác vụ không đồng bộ, là tác vụ chạy "trong nền", sao cho người gọi không chặn cho đến khi nó nhận được phản hồi. Việc tạo ra một quá trình ngừng hoạt động là một ví dụ về nơi có thể đạt được sự bất đồng bộ như vậy.
Tomasz Bartkowiak

điều gì sẽ xảy ra nếu /render/<id>điểm cuối mong đợi một cái gì đó là kết quả của my_func()?
Will Gu

my_funcVí dụ, bạn có thể thực hiện gửi phản hồi / nhịp tim tới một số điểm cuối khác. Hoặc bạn có thể thiết lập và chia sẻ một số hàng đợi tin nhắn mà qua đó bạn có thể giao tiếpmy_func
Tomasz Bartkowiak.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.