ফ্লাস্কে একটি অ্যাসিক্রোনাস টাস্ক করা


104

আমি ফ্লাস্কে একটি অ্যাপ্লিকেশন লিখছি, যা WSGIসিঙ্ক্রোনাস এবং ব্লকিং ব্যতীত সত্যই ভাল কাজ করে। আমার বিশেষত একটি কাজ রয়েছে যা একটি তৃতীয় পক্ষের এপিআইকে কল করে এবং সেই কাজটি শেষ হতে কয়েক মিনিট সময় নিতে পারে। আমি সেই কলটি করতে চাই (এটি আসলে কলগুলির একটি সিরিজ) এবং এটি চালিয়ে যেতে দিন। নিয়ন্ত্রণ ফ্লাস্কে ফিরে আসে যখন।

আমার মতামতটি দেখে মনে হচ্ছে:

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

পদ্ধতিটি ফিরে আসার পরে চালানো এবং সম্পাদনের জন্য একটি কলব্যাক সরবরাহ করুন, যখন ফ্লাস্ক অনুরোধগুলির প্রক্রিয়া চালিয়ে যেতে পারে। এটিই একমাত্র কাজ যা অ্যাসিক্রোনালিকভাবে চালানোর জন্য আমার ফ্লাস্কের প্রয়োজন এবং এটি কীভাবে কার্যকর করা যায় সে সম্পর্কে আমি কিছু পরামর্শ চাই।

আমি টুইস্টেড এবং ক্লেইনের দিকে নজর রেখেছি, তবে আমি নিশ্চিত নই যে তারা ওভারকিল করেছে, কারণ থ্রেডিং যথেষ্ট হবে। বা সম্ভবত সেলারি এর জন্য ভাল পছন্দ?


আমি সাধারণত এর জন্য সেলারি ব্যবহার করি ... এটি অত্যধিক কিল হতে পারে তবে আফাইক থ্রেডিং ওয়েব পরিবেশে (আইরিক ...) ভাল কাজ করে না
জোড়ান বিসলে

ঠিক। হ্যাঁ - আমি কেবল সিলারিটি অনুসন্ধান করছিলাম। এটি একটি ভাল পদ্ধতির হতে পারে। ফ্লাস্ক দিয়ে কার্যকর করা সহজ?
ডারউইন টেক

হেই আমি সকেট সার্ভারও ব্যবহার করতে থাকি (ফ্লাস্ক-সোকেটিও) এবং হ্যাঁ আমি ভেবেছিলাম এটি বেশ সহজ ... সবচেয়ে শক্ত অংশটি সবকিছু ইনস্টল করা হচ্ছে
জোড়ান বিসলে

4
আমি পরীক্ষা সুপারিশ করবে এই বাইরে। এই লোকটি সাধারণভাবে ফ্লাস্কের জন্য দুর্দান্ত টিউটোরিয়াল লেখেন এবং এটি কীভাবে ফ্ল্যাস্ক অ্যাপ্লিকেশনে অ্যাসিক্রোনাস টাস্কগুলিকে সংহত করতে হয় তা বোঝার জন্য এটি দুর্দান্ত।
অ্যাটলস্পিন

উত্তর:


106

আপনার জন্য অ্যাসিক্রোনাস টাস্কটি পরিচালনা করতে আমি সেলারি ব্যবহার করব। আপনার টাস্ক সারি হিসাবে পরিবেশন করতে আপনাকে ব্রোকার ইনস্টল করতে হবে (রাবিটএমকিউ এবং রেডিস প্রস্তাবিত)।

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
    )

আপনার ফ্লাস্ক অ্যাপটি চালান এবং আপনার সেলারি কর্মী চালানোর জন্য অন্য প্রক্রিয়া শুরু করুন।

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

আমি ফ্লাস্কের সাথে সিলারি ব্যবহারের আরও গভীরতার গাইডের জন্য মিগুয়েল গ্রিংবার্গের লেখার বিষয়েও উল্লেখ করব ।


সেলারি একটি কঠিন সমাধান, তবে এটি কোনও হালকা ওজনের সমাধান নয় এবং সেট আপ করতে কিছুটা সময় নেয়।
wobbily_col

34

থ্রেডিং অন্য সম্ভাব্য সমাধান। যদিও সেলারি ভিত্তিক সমাধান মাপের অ্যাপ্লিকেশনগুলির জন্য ভাল, যদি আপনি প্রশ্নের শেষ প্রান্তে খুব বেশি ট্র্যাফিকের প্রত্যাশা না করেন তবে থ্রেডিং একটি কার্যকর বিকল্প।

এই দ্রবণটি মিগুয়েল গ্রিনবার্গের পাইকন 2016 ফ্লাস্ক এ স্কেল উপস্থাপনার উপর ভিত্তি করে তৈরি করা হয়েছে , বিশেষত তার স্লাইড ডেকে 41 স্লাইড। আসল উত্সে আগ্রহী ব্যক্তিদের জন্য তাঁর কোডটি গিথুবেও উপলব্ধ

একটি ব্যবহারকারীর দৃষ্টিকোণ থেকে কোড নিম্নলিখিত হিসাবে কাজ করে:

  1. আপনি শেষ পয়েন্টে কল করেন যা দীর্ঘদিনের কাজটি সম্পাদন করে।
  2. এই শেষ পয়েন্টটি 202 টাস্কের স্থিতি পরীক্ষা করার জন্য একটি লিঙ্ক সহ স্বীকৃত প্রদান করে।
  3. স্থিতি লিঙ্কে কলগুলি যখন টাকগুলি এখনও চলছে তখন 202 ফেরত দেয় এবং 200 টি (এবং ফলাফল) কাজ শেষ হয়ে গেলে ফিরে আসে।

একটি এপিআই কলকে একটি পটভূমির কাজে রূপান্তর করতে, কেবলমাত্র @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: ত্রুটিটি শেষ হয়েছে: 'gettaskstatus' মানগুলির সাথে ['টাস্ক_আইডি' সহ ইউআরএল তৈরি করা যায়নি] আমি কি কিছু হারিয়েছি?
নিকোলাস দুফর

15

আপনি এর multiprocessing.Processসাথে ব্যবহার করার চেষ্টা করতে পারেন daemon=True; process.start()পদ্ধতি ব্লক করে না এবং আপনার কলার করার সময় পটভূমিতে আপনার ব্যয়বহুল ফাংশন executes অবিলম্বে একটি প্রতিক্রিয়া / স্থিতি ফিরে আসতে পারেন।

ফ্যালকন ফ্রেমওয়ার্কের সাথে কাজ করার সময় এবং 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প্রক্রিয়াগুলি কোনও শিশু প্রক্রিয়া উত্সাহিত করার অনুমতিপ্রাপ্ত নয়।


অ্যাসিঙ্ক্রোনাস একটি নির্দিষ্ট ধরণের সম্মতি যা থ্রেডিং বা মাল্টিপ্রসেসিং নয় is থ্রেডিং হ'ল
অ্যাসিঙ্ক

4
আমি আপনার বক্তব্য বুঝতে পারি না। লেখক একটি অ্যাসিক্রোনাস টাস্কের বিষয়ে কথা বলছেন, যা টাস্ক যা "পটভূমিতে" চালায়, যেমন কলার তার প্রতিক্রিয়া না পাওয়া পর্যন্ত অবরুদ্ধ হয় না। ডিমন প্রক্রিয়া তৈরি করা একটি উদাহরণ যেখানে এই জাতীয় সংশ্লেষবাদ অর্জন করা যায়।
টমাস বার্টকোইয়াক

/render/<id>শেষবিন্দু ফলাফল হিসাবে কিছু প্রত্যাশা যদি my_func()?
গুগ

my_funcউদাহরণস্বরূপ আপনি অন্য কোনও শেষ পয়েন্টে প্রতিক্রিয়া / হার্টবিটগুলি প্রেরণ করতে পারেন । অথবা আপনি এমন কিছু বার্তা সারি স্থাপন এবং ভাগ করে নিতে পারেন যার মাধ্যমে আপনি যোগাযোগ করতে পারেনmy_func
টমসজ বার্তকোয়িয়াক
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.