【问题标题】:Using a Celery worker to interact with a SQLAlchemy DB, including knowing the user from the request使用 Celery worker 与 SQLAlchemy DB 交互,包括从请求中了解用户
【发布时间】:2019-07-17 02:43:55
【问题描述】:

我对此进行了大量研究,包括尝试像this 这样的答案。看来 Celery 无法访问我的 Flask 应用程序的上下文。

我非常了解我的 celery 对象,我的任务将由什么来装饰,必须能够访问我的 Flask 应用程序的上下文。我确实相信它应该,因为我按照this 指南创建了我的芹菜对象。我不确定混淆是否在于我使用 Flask-HTTPAuth 的事实。

这是我拥有的一些东西。

def make_celery(app):
    celery = Celery(app.import_name, backend=app.config["CELERY_RESULT_BACKEND"], broker=app.config["CELERY_BROKER_URL"])
    celery.conf.update(app.config)
    TaskBase = celery.Task
    class ContextTask(TaskBase):
        abstract = True
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return TaskBase.__call__(self, *args, **kwargs)
    celery.Task = ContextTask
    return celery

app = Flask(__name__)
auth = HTTPBasicAuth()
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///flask_app.db"
app.config["CELERY_BROKER_URL"] = "redis://localhost:6379"
app.config["CELERY_RESULT_BACKEND"] = "redis://localhost:6379"
celery = make_celery(app)
db = SQLAlchemy(app)

@celery.task(bind=True, name="flask_app.item_loop")
def loop(self):
    items = g.user.items
    for item in items:
        print(item)

不过,使用 Flask 运行这个任务是不行的。我尝试通过点击服务器来启动这个功能(在授权的情况下!)。

@app.route("/item_loop")
@auth.login_required
def item_loop():
    result = loop.delay()
    return "It's running."

但是 Celery 工人告诉我任务 raised unexpected: AttributeError("'_AppCtxGlobals' object has no attribute 'user'",),我相信这意味着,如上所述,我的 celery 对象没有应用程序上下文,即使我使用了推荐的工厂模式。

【问题讨论】:

    标签: python flask celery flask-sqlalchemy flask-httpauth


    【解决方案1】:

    虽然 Dave 和 Greg 的回答中的建议是有效的,但他们没有强调的是您对在 Celery 任务中使用应用程序上下文的误解。

    您有一个 Flask 应用程序,您在其中使用 Flask-HTTPAuth。您可能有一个verify_password 处理程序,它将g.user 设置为经过身份验证的用户。这意味着当您处理请求时,您可以以g.user 的身份访问用户。这一切都很好。

    您还拥有一个或多个 Celery 工作者,它们是独立的进程,与 Flask 服务器没有直接连接。 Flask 服务器和 Celery 工作进程之间的唯一通信发生在您使用的消息代理(通常是 Redis 或 RabbitMQ)上。

    根据您的需要,Celery 工作人员可能需要访问 Flask 应用程序。这在使用将配置存储在 app.config 字典中的 Flask 扩展时很常见。需要此功能的两个常见扩展是 Flask-SQLAlchemy 和 Flask-Mail。如果不访问app.config,Celery 任务将无法打开与数据库的连接或发送电子邮件,因为它不知道数据库和/或电子邮件服务器的详细信息。

    为了让 Celery 工作人员访问配置,公认的做法是在每个工作人员中创建重复的 Flask 应用程序。这些是辅助应用程序,它们与主 Flask 服务器使用的实际应用程序对象没有任何关联。它们的唯一目的是保存原始 app.config 字典的副本,您的任务或您的任务正在使用的任何 Flask 扩展都可以访问该字典。

    因此,期望在 Flask 服务器中设置的 g.user 在 Celery 任务中也可以作为 g.user 访问是无效的,因为这些是来自不同应用程序实例的不同 g 对象。

    如果您需要在 Celery 任务中使用经过身份验证的用户,您应该将user_id(通常是g.user.id)作为参数传递给您的任务。然后在您的任务中,您可以使用此id 从数据库加载用户。希望这会有所帮助!

    【讨论】:

    • 我认为我遵循。我已经确认,将用户加载到任务中对我来说非常有意义并且会起作用。实际上写回数据库似乎是问题所在。很抱歉,我无法完全掌握这一点。你提到的重复的 Flask 应用程序就是这样出现的吗?
    • 如果您按照我在答案中指出的那样创建辅助应用程序,则从数据库读取或写入应该没有问题。如果您遇到特定错误,则需要在问题中对此进行扩展。
    • 虽然我不确定您所说的创建辅助应用程序是什么意思,但我想我做到了,因为我得到了它的工作,只是真正研究了提供的模式。谢谢。
    • 我所说的“次要”是指运行服务器的不是 Flask 实例。
    【解决方案2】:

    要从任务执行中检索用户,您可以尝试传递用户对象(如果 celery 可以腌制它),或者传递足够的信息以使任务可以检索用户对象(例如用户的 id)。在后一种情况下,您的任务将类似于

    @celery.task(bind=True, name="flask_app.item_loop")
    def loop(self, user_id):
        user = User.query.get(user_id)
        items = user.items
        for item in items:
            print(item)
    

    然后你会通过

    启动它(假设你使用的是flask_login)
    result = loop.delay(current_user.id)
    

    【讨论】:

    • 嗨,这是有道理且有效的,但我如何才能写回 SQLAlchemy DB? db.session.commit() 不起作用:name 'db' is not defined
    • 那么你需要一个与 Flask 应用程序具有相同数据库设置的数据库。请参阅*.com/questions/12044776/… 了解一种方法。
    【解决方案3】:

    正如@Dave W. Smith 所指出的,与其依赖g 来检索用户,不如将用户信息作为参数传递给 Celery 任务可能是一种更好的方法。根据Flask documentation on app contextg 的生命周期是一个请求。由于 Celery 任务是异步执行的,因此它将在与您定义用户的请求中不同的应用上下文中执行。

    【讨论】: