Еще одно возможное решение - многопоточность. Хотя решение на основе Celery лучше для масштабируемых приложений, если вы не ожидаете слишком большого трафика на рассматриваемой конечной точке, потоки - жизнеспособная альтернатива.
Это решение основано на презентации PyCon 2016 Flask at Scale Мигеля Гринберга , в частности, на слайде 41 в его колоде слайдов. Его код также доступен на github для тех, кто интересуется первоисточником.
С точки зрения пользователя код работает следующим образом:
- Вы вызываете конечную точку, которая выполняет длительную задачу.
- Эта конечная точка возвращает 202 Accepted со ссылкой для проверки статуса задачи.
- Вызов ссылки статуса возвращает 202, пока таксы все еще выполняются, и возвращает 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:
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)