Flask 工厂模式下配置Celery

178 阅读5分钟

背景:遇到了部分代码执行时间长的问题,想要通过类似异步执行的策略将耗时长的代码块在后台单独执行。在网上搜索了解决方案,使用celery的比较多,于是开始引入celery。但由于大部分教程不是工厂模式的情景没有配置成功(也问了chatgpt但他一通乱答)。会遇到循环引入之类的问题。最终谷歌找到了完整教程转载在这里。原文后续还有socket配置,后面尝试。 框架:Flask
前提:你已经配置好了Redis
原文链接testdriven.io/courses/fla…
**这是转载!这是转载!这是转载!这是转载!这是转载! **

这是我的项目根目录下的文件结构,本次涉及到的有

  1. app/config/init.py
  2. app/utils/celerySetup.py
  3. app/task.py
  4. app/init.py
  5. run.py image.png

安装Celery和Flask-CeleryExt

pip install celery
pip install flask-celeryext==0.4.3

我的版本是 celery==5.4.0 Flask-CeleryExt==0.4.3

加入Celery配置项到配置文件

app/config/init.py

CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://127.0.0.1:6379/0")
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://127.0.0.1:6379/0") 

为避免后面配置引入有问题,附上我的配置文件格式

import os

class Config:
    ## 你的内容
    CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://127.0.0.1:6379/1")  
    CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://127.0.0.1:6379/2")  
    ## 你的内容
    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    ## 填开发环境下的配置项或pass(略过)
class TestingConfig(Config):
    pass

class ProductionConfig(Config):
    

Celery应用文件

app/utils/celerySetup.py

from celery import current_app as current_celery_app
def make_celery(app):
    celery = current_celery_app
    celery.config_from_object(app.config, namespace="CELERY")
    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)
    celery.Task = ContextTask
    return celery

原文:

  1. make_celery是一个工厂函数,它配置并返回一个 Celery 应用程序实例。
  2. 我们没有创建新的 Celery 实例,而是使用current_app,以便共享任务按预期工作。
  3. celery.config_from_object(app.config, namespace="CELERY")表示所有与 Celery 相关的配置键都应该大写并以 为前缀CELERY_。例如,要配置broker_url,您应该使用CELERY_BROKER_URL
  4. 注意这边新版也推荐配置键小写例如celery_broker_url

创建异步任务

app/task.py

from celery import shared_task
import time
from celery.result import AsyncResult

# 异步任务
@shared_task
def add_together(a, b):
    print('------------------------------------------')
    time.sleep(5)
    return a + b

# 根据异步任务的id读取结果
def task_result(id):
    result = AsyncResult(id)
    return {
        "ready": result.ready(),
        "successful": result.successful(),
        "value": result.result if result.ready() else None,
    }

原文:

  1. 网络上的许多资源都建议使用celery.task。在某些情况下,这可能会导致循环导入,因为您必须导入 Celery 实例。
  2. 我们过去常常shared_task让我们的代码可重复使用,这同样需要current_appinmake_celery而不是创建新的 Celery 实例。现在,我们可以将此文件复制到应用程序中的任何位置,它将按预期工作。

工厂函数create_app的修改

app/init.py 以下是涉及部分的代码,其中test_config,ProductionConfig,DevelopmentConfig是我配置文件中的变量引入其他配置的,Celery的两个配置项直接写在配置文件的基础配置中就好,之后有需要在根据运行环境变更

from flask import Flask
import redis
from .config import *
from flask_celeryext import FlaskCeleryExt
from project.celery_utils import make_celery   #引入第二步创建的celery文件

## 你自己的内容
ext_celery = FlaskCeleryExt(create_celery_app=make_celery)  
## 你自己的内容

##  注意这边才是工厂函数,ext_celery在外部才能被其他文件引用

def create_app(test_config=None, **kwargs):
    # create and configure the app
    app = Flask(__name__, instance_relative_config=True)
    # Select configuration
    env_config = os.getenv('FLASK_ENV', 'development')
    if test_config:
        app.config.from_object(test_config)
    elif env_config == 'production':
        app.config.from_object(ProductionConfig)
    else:
        app.config.from_object(DevelopmentConfig)
    # Load instance config 其中的配置项会覆盖当前
    app.config.from_pyfile('config.py', silent=True)
    # ensure the instance folder exists
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # Initialize Redis
    app.redis = redis.StrictRedis.from_url(app.config['REDIS_URL'])
    ext_celery.init_app(app)


    from app.api.test import bp as test_bp
    app.register_blueprint(test_bp)

    return app

原文: 当我们实例化时ext_celery,我们将自定义应用程序工厂传递make_celery给它。如果我们没有这样做,它FlaskCeleryExt会自动为我们创建 Celery 应用程序,但由于我们将运行共享任务,因此不建议这样做。

修改run.py

run.py(和app文件夹同级!!)
这是我的flask项目启动文件

from app import create_app,ext_celery

app = create_app()
celery = ext_celery.celery

if __name__ == '__main__':
    app.run(debug=True)

测试

1. 设置测试接口调用异步函数

测试我这边用了两个测试接口,这是我的测试接口文件
文件目录:app/api/test.py
在create_app()中记得注册蓝图(前文代码包含)

from flask import (
    Blueprint,request
)
from ..service.function import random_int_list
from app.task import add_together, task_result # 引入前文task.py中的异步函数和读取函数

bp = Blueprint('test', __name__, url_prefix='/api/test')


@bp.route('/')
def index():
    return random_int_list(0, 100, 100)


@bp.route('/a')
def celery_test():
    res = add_together.delay(1, 2)
    return {"result_id": res.id}


@bp.route('/b', methods=['GET'])
def celery_get():
    task_id = request.args.get('task_id')
    res = task_result(task_id)
    return res

2. 启动flask应用

我这边是执行run.py。无论如何启动切记确保程序执行到了run.py中加入的

from app import create_app,ext_celery
celery = ext_celery.celery

启动成功 image.png

4. 启动celery应用

在新终端执行一下命令启动celery

celery -A run.celery worker --loglevel=info -P threads

启动成功截图如下,可能会有一些警告,注意判别是否影响运行 image.png 注意这边原文和大部分教程都建议用

celery -A run.celery worker --loglevel=info

但在这里会造成无法确定线程导致无法进行任务(如下图),所以加了-P thread指定线程

image.png

5. 调用测试接口

调用app/api/test.py中定义的接口测试,在浏览器访问 127.0.0.1:5000/api/test/a获得返回值'result_id' image.png 再使用这个result_id作为异步任务的task_id访问b接口查看异步任务执行结果。如图返回格式与我们在app/task.py中定义的返回格式一致。这里的ready代表任务是否结束。

image.png 异步任务成功在celery中运行 image.png 这边有个问题,从截图中看任务被执行了好几次,没找到原因,有无大神解答。 看其他项目有配置全局调用接口时判断是否异步,之后研究一下。以上,请大家指正1