【问题标题】:Python Flask with celery out of application context带有芹菜的Python Flask超出应用程序上下文
【发布时间】:2013-04-19 17:36:52
【问题描述】:

我正在使用 python Flask 构建一个网站。一切都很顺利,现在我正在尝试实施 celery。

在我尝试使用 celery 的 flask-mail 发送电子邮件之前,情况一直很好。现在我收到“在应用程序上下文之外工作”错误。

完整的回溯是

  Traceback (most recent call last):
  File "/usr/lib/python2.7/site-packages/celery/task/trace.py", line 228, in trace_task
    R = retval = fun(*args, **kwargs)
  File "/usr/lib/python2.7/site-packages/celery/task/trace.py", line 415, in __protected_call__
    return self.run(*args, **kwargs)
  File "/home/ryan/www/CG-Website/src/util/mail.py", line 28, in send_forgot_email
    msg = Message("Recover your Crusade Gaming Account")
  File "/usr/lib/python2.7/site-packages/flask_mail.py", line 178, in __init__
    sender = current_app.config.get("DEFAULT_MAIL_SENDER")
  File "/usr/lib/python2.7/site-packages/werkzeug/local.py", line 336, in __getattr__
    return getattr(self._get_current_object(), name)
  File "/usr/lib/python2.7/site-packages/werkzeug/local.py", line 295, in _get_current_object
    return self.__local()
  File "/usr/lib/python2.7/site-packages/flask/globals.py", line 26, in _find_app
    raise RuntimeError('working outside of application context')
RuntimeError: working outside of application context

这是我的邮件功能:

@celery.task
def send_forgot_email(email, ref):
    global mail
    msg = Message("Recover your Crusade Gaming Account")
    msg.recipients = [email]
    msg.sender = "Crusade Gaming stuff@cg.com"
    msg.html = \
        """
        Hello Person,<br/>

        You have requested your password be reset. <a href="{0}" >Click here recover your account</a> or copy and paste this link in to your browser: {0} <br />

        If you did not request that your password be reset, please ignore this.
        """.format(url_for('account.forgot', ref=ref, _external=True))
    mail.send(msg)

这是我的芹菜文件:

from __future__ import absolute_import

from celery import Celery

celery = Celery('src.tasks',
                broker='amqp://',
                include=['src.util.mail'])


if __name__ == "__main__":
    celery.start()

【问题讨论】:

  • mail 是 flask_mail 实例。应用程序启动时,邮件从不同的文件启动。
  • 我不知道哪个解决方案会更好,将上下文添加到整个 celery 应用程序实例或只是回调函数。但是您可以在flask.pocoo.org/docs/appcontext 阅读有关 Flask 应用程序上下文的所有信息

标签: python flask celery


【解决方案1】:

这是一个与烧瓶应用程序工厂模式一起使用的解决方案,还可以创建带有上下文的 celery 任务,而无需使用app.app_context()。在避免循环导入的同时获取该应用程序确实很棘手,但这解决了它。这是针对撰写本文时最新的 celery 4.2 的。

结构:

repo_name/
    manage.py
    base/
    base/__init__.py
    base/app.py
    base/runcelery.py
    base/celeryconfig.py
    base/utility/celery_util.py
    base/tasks/workers.py

所以base 是本例中的主要应用程序包。在base/__init__.py 中,我们创建 celery 实例如下:

from celery import Celery
celery = Celery('base', config_source='base.celeryconfig')

base/app.py 文件包含烧瓶应用工厂create_app 并注意它包含的init_celery(app, celery)

from base import celery
from base.utility.celery_util import init_celery

def create_app(config_obj):
    """An application factory, as explained here:
    http://flask.pocoo.org/docs/patterns/appfactories/.
    :param config_object: The configuration object to use.
    """
    app = Flask('base')
    app.config.from_object(config_obj)
    init_celery(app, celery=celery)
    register_extensions(app)
    register_blueprints(app)
    register_errorhandlers(app)
    register_app_context_processors(app)
    return app

转到base/runcelery.py 内容:

from flask.helpers import get_debug_flag
from base.settings import DevConfig, ProdConfig
from base import celery
from base.app import create_app
from base.utility.celery_util import init_celery
CONFIG = DevConfig if get_debug_flag() else ProdConfig
app = create_app(CONFIG)
init_celery(app, celery)

接下来,base/celeryconfig.py 文件(例如):

# -*- coding: utf-8 -*-
"""
Configure Celery. See the configuration guide at ->
http://docs.celeryproject.org/en/master/userguide/configuration.html#configuration
"""

## Broker settings.
broker_url = 'pyamqp://guest:guest@localhost:5672//'
broker_heartbeat=0

# List of modules to import when the Celery worker starts.
imports = ('base.tasks.workers',)

## Using the database to store task state and results.
result_backend = 'rpc'
#result_persistent = False

accept_content = ['json', 'application/text']

result_serializer = 'json'
timezone = "UTC"

# define periodic tasks / cron here
# beat_schedule = {
#    'add-every-10-seconds': {
#        'task': 'workers.add_together',
#        'schedule': 10.0,
#        'args': (16, 16)
#    },
# }

现在在base/utility/celery_util.py 文件中定义init_celery:

# -*- coding: utf-8 -*-

def init_celery(app, celery):
    """Add flask app context to celery.Task"""
    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

base/tasks/workers.py的工人:

from base import celery as celery_app
from flask_security.utils import config_value, send_mail
from base.bp.users.models.user_models import User
from base.extensions import mail # this is the flask-mail

@celery_app.task
def send_async_email(msg):
    """Background task to send an email with Flask-mail."""
    #with app.app_context():
    mail.send(msg)

@celery_app.task
def send_welcome_email(email, user_id, confirmation_link):
    """Background task to send a welcome email with flask-security's mail.
    You don't need to use with app.app_context() here. Task has context.
    """
    user = User.query.filter_by(id=user_id).first()
    print(f'sending user {user} a welcome email')
    send_mail(config_value('EMAIL_SUBJECT_REGISTER'),
              email,
              'welcome', user=user,
              confirmation_link=confirmation_link) 

然后,您需要从 repo_name 文件夹中在两个不同的 cmd 提示符中启动 celery beat 和 celery worker。

在一个 cmd 提示符下执行 celery -A base.runcelery:celery beat 和另一个 celery -A base.runcelery:celery worker

然后,运行需要烧瓶上下文的任务。应该可以。

【讨论】:

  • 困扰了我好几天。非常感谢
  • 为什么需要运行两个 celery 进程?谢谢!
  • @MinhHoàng celery -A base.runcelery: celery beat 运行周期性任务/cron 作业,前提是您已在 base.celeryconfig.py 文件中设置它们,具体参考我已在例子。如果您不需要使用任何周期性任务,那么您不需要运行两个 celery 进程。你可以只运行celery -A base.runcelery: celery worker 进程。
  • create_app 函数中的init_celery(app, celery=celery) 有什么作用吗?
  • @Naman 是的,它将烧瓶应用程序上下文添加到 celery.Task
【解决方案2】:

Flask-mail 需要 Flask 应用程序上下文才能正常工作。在 celery 端实例化 app 对象并像这样使用 app.app_context:

with app.app_context():
    celery.start()

【讨论】:

  • 如何让 celery 访问烧瓶应用程序?我现在将它们放在单独的文件中,有问题吗?
  • 像运行 Flask 时一样将应用程序导入到 celery 文件中。您可能需要为您的 Flask 应用程序发布您的__init__.py,或者包括您的设置的更多详细信息,以便我更具体。
  • 有同样的问题,但实例化应用程序并在上下文中启动芹菜是行不通的。可能是因为任务实例是在上下文之外创建的?
  • 这只会在当前线程中起作用。您可能在另一个线程中运行某些东西。只需在其中做同样的事情。
【解决方案3】:

我没有任何意见,所以我无法支持@codegeek 的上述答案,所以我决定自己写,因为我搜索这样的问题得到了这个问题/答案的帮助:我刚刚尝试在 python/flask/celery 场景中解决类似问题取得了一些成功。即使您的错误是由于尝试使用 mail 而我的错误是尝试在 celery 任务中使用 url_for,我怀疑这两者与同一个问题有关,并且您会因使用url_for 如果您在 mail 之前尝试过使用它。

在 celery 任务中没有应用程序的上下文(即使在包含 import app from my_app_module 之后)我也遇到了错误。您需要在应用的上下文中执行mail 操作:

from module_containing_my_app_and_mail import app, mail    # Flask app, Flask mail
from flask.ext.mail import Message    # Message class

@celery.task
def send_forgot_email(email, ref):
    with app.app_context():    # This is the important bit!
        msg = Message("Recover your Crusade Gaming Account")
        msg.recipients = [email]
        msg.sender = "Crusade Gaming stuff@cg.com"
        msg.html = \
        """
        Hello Person,<br/>
        You have requested your password be reset. <a href="{0}" >Click here recover your account</a> or copy and paste this link in to your browser: {0} <br />
        If you did not request that your password be reset, please ignore this.
        """.format(url_for('account.forgot', ref=ref, _external=True))

        mail.send(msg)

如果有人有兴趣,我在 celery 任务中使用url_for的问题的解决方法可以找到here

【讨论】:

  • 不会创建循环导入吗?您需要从您的应用中导入上面的 sn-p,对吧?
【解决方案4】:

在您的 mail.py 文件中,导入您的“app”和“mail”对象。然后,使用请求上下文。做这样的事情:

from whateverpackagename import app
from whateverpackagename import mail

@celery.task
def send_forgot_email(email, ref):
    with app.test_request_context():
        msg = Message("Recover your Crusade Gaming Account")
        msg.recipients = [email]
        msg.sender = "Crusade Gaming stuff@cg.com"
        msg.html = \
        """
        Hello Person,<br/>
        You have requested your password be reset. <a href="{0}" >Click here recover your account</a> or copy and paste this link in to your browser: {0} <br />
        If you did not request that your password be reset, please ignore this.
        """.format(url_for('account.forgot', ref=ref, _external=True))

        mail.send(msg)

【讨论】:

  • 我认为在非测试环境中使用 test_request_context 并不是一个好主意。
  • 这解决了将 Flask-Babel 与 celery 结合使用时的问题。 Flask-Babel 不会在没有请求的情况下加载任何翻译(因为它会在请求上下文中缓存翻译)。除此之外,Flask-Babel 可以在没有请求的情况下正常工作。所以使用test_request_context() 只是构造函数上下文的一种简单方法,尽管它可能有点浪费。
【解决方案5】:

不使用app.app_context(),只需在注册蓝图之前配置 celery,如下所示:

celery = Celery('myapp', broker='redis://localhost:6379/0', backend='redis://localhost:6379/0')

从您希望使用 celery 的蓝图中,调用已创建的 celery 实例来创建您的 celery 任务。

它将按预期工作。

【讨论】:

    猜你喜欢
    • 2016-10-23
    • 2018-12-17
    • 1970-01-01
    • 2016-03-22
    • 1970-01-01
    • 2018-10-03
    • 2017-06-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多