Threading adalah solusi lain yang mungkin. Meskipun solusi berbasis Celery lebih baik untuk aplikasi dalam skala besar, jika Anda tidak mengharapkan terlalu banyak lalu lintas pada titik akhir yang dipermasalahkan, threading adalah alternatif yang layak.
Solusi ini didasarkan pada presentasi PyCon 2016 Flask at Scale milik Miguel Grinberg , khususnya slide 41 di dek slide miliknya. Kodenya juga tersedia di github bagi mereka yang tertarik dengan sumber aslinya.
Dari perspektif pengguna, kode berfungsi sebagai berikut:
- Anda melakukan panggilan ke titik akhir yang melakukan tugas yang berjalan lama.
- Titik akhir ini mengembalikan 202 Diterima dengan tautan untuk memeriksa status tugas.
- Panggilan ke link status mengembalikan 202 saat taks masih berjalan, dan mengembalikan 200 (dan hasilnya) saat tugas selesai.
Untuk mengonversi panggilan api menjadi tugas latar belakang, cukup tambahkan dekorator @async_api.
Berikut adalah contoh lengkapnya:
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:
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):
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:
tasks[task_id]['return_value'] = InternalServerError()
if current_app.debug:
raise
finally:
tasks[task_id]['completion_timestamp'] = datetime.timestamp(datetime.utcnow())
task_id = uuid.uuid4().hex
tasks[task_id] = {'task_thread': threading.Thread(
target=task_call, args=(current_app._get_current_object(),
request.environ))}
tasks[task_id]['task_thread'].start()
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=''):
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)