Python Flask with celery out of application context
Asked Answered
S

6

18

I am building a website using python Flask. Everything is going good and now I am trying to implement celery.

That was going good as well until I tried to send an email using flask-mail from celery. Now I am getting an "working outside of application context" error.

full traceback is

  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

This is my mail function:

@celery.task
def send_forgot_email(email, ref):
    global mail
    msg = Message("Recover your Crusade Gaming Account")
    msg.recipients = [email]
    msg.sender = "Crusade Gaming [email protected]"
    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)

This is my celery file:

from __future__ import absolute_import

from celery import Celery

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


if __name__ == "__main__":
    celery.start()
Sciential answered 25/4, 2013 at 17:42 Comment(2)
mail is the flask_mail instance. mail gets initiated from a different file when the app gets started.Sciential
I don't know which solution would be better, adding the context to the whole celery application instance or just the callback function. But you can read all about the Flask Application Context at flask.pocoo.org/docs/appcontextStylography
O
21

Here is a solution which works with the flask application factory pattern and also creates celery task with context, without needing to use app.app_context(). It is really tricky to get that app while avoiding circular imports, but this solves it. This is for celery 4.2 which is the latest at the time of writing.

Structure:

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

So base is the main application package in this example. In the base/__init__.py we create the celery instance as below:

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

The base/app.py file contains the flask app factory create_app and note the init_celery(app, celery) it contains:

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

Moving on to base/runcelery.py contents:

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)

Next, the base/celeryconfig.py file (as an example):

# -*- 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)
#    },
# }

Now define the init_celery in the base/utility/celery_util.py file:

# -*- 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

For the workers in 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) 

Then, you need to start the celery beat and celery worker in two different cmd prompts from inside the repo_name folder.

In one cmd prompt do a celery -A base.runcelery:celery beat and the other celery -A base.runcelery:celery worker.

Then, run through your task that needed the flask context. Should work.

Obmutescence answered 3/6, 2018 at 10:58 Comment(6)
It has bothered me for a few days. Thanks a lotToggle
Why need to run two celery processes? Thank you!Alarum
@MinhHoàng the celery -A base.runcelery: celery beat runs the periodic tasks / cron jobs, only if you have set them up in the base.celeryconfig.py file, specifically refer to beat_schedule variable which I have commented out in the example. If you don't need to use any periodic tasks then you do not need to run two celery processes. You can just run the celery -A base.runcelery: celery worker process.Obmutescence
Does the init_celery(app, celery=celery) inside create_app function do anything?Disaccord
@Disaccord yes it adds the flask application context to celery.TaskObmutescence
@MinhHoàng, mb to separate app contextZamboanga
P
6

Flask-mail needs the Flask application context to work correctly. Instantiate the app object on the celery side and use app.app_context like this:

with app.app_context():
    celery.start()
Plentiful answered 25/4, 2013 at 18:10 Comment(4)
How do i give celery access to the flask app? I have them in separate files right now, is that wrong?Sciential
Import the app into the celery file like you would do when running Flask. You might have to post your __init__.py for your Flask app or include more details of your setup for me to be more specific.Plentiful
Having the same problem, but instantiating the app and starting the celery inside the context like that isn't working. maybe because the task instance was created outside of the context?Tomaso
This will only work in the current thread. It's possible you have something in running in another thread. Just do the same thing in that one.Plentiful
T
3

I don't have any points, so I couldn't upvote @codegeek's above answer, so I decided to write my own since my search for an issue like this was helped by this question/answer: I've just had some success trying to tackle a similar problem in a python/flask/celery scenario. Even though your error was from trying to use mail while my error was around trying to use url_for in a celery task, I suspect the two were related to the same problem and that you would have had errors stemming from the use of url_for if you had tried to use that before mail.

With no context of the app present in a celery task (even after including an import app from my_app_module) I was getting errors, too. You'll need to perform the mail operation in the context of the app:

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 [email protected]"
        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)

If anyone is interested, my solution for the problem of using url_for in celery tasks can be found here

Tridimensional answered 25/12, 2014 at 16:30 Comment(1)
Won't that create a circular import? You would need to import the snippet above from your app, right?Paperboy
K
3

The answer provided by Bob Jordan is a good approach but I found it very hard to read and understand so I completely ignored it only to arrive at the same solution later myself. In case anybody feels the same, I'd like to explain the solution in a much simpler way. You need to do 2 things:

  1. create a file which initializes a Celery app
# celery_app_file.py

from celery import Celery

celery_app = Celery(__name__)
  1. create another file which initializes a Flask app and uses it to monkey-patch the Celery app created earlier
# flask_app_file.py

from flask import Flask
from celery_app import celery_app

flask_app = Flask(__name__)

class ContextTask(celery_app.Task):
    def __call__(self, *args, **kwargs):
        with flask_app.app_context():
            if self.abstract:
                return super().__call__(*args, **kwargs)
            else:
                return super().__call__(self, *args, **kwargs)
celery_app.Task = ContextTask

Now, any time you import the Celery application inside different files (e.g. mailing/tasks.py containing email-related stuff, or database/tasks.py containg database-related stuff), it'll be the already monkey-patched version that will work within a Flask context.

The important thing to remember is that this monkey-patching must happen at some point when you start Celery through the command line. This means that (using my example) you have to run celery -A flask_app_file.celery_app worker because flask_app_file.py is the file that contains the celery_app variable with a monkey-patched Celery application assigned to it.

Klausenburg answered 11/9, 2022 at 18:41 Comment(0)
D
2

In your mail.py file, import your "app" and "mail" objects. Then, use request context. Do something like this:

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 [email protected]"
        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)
Diatropism answered 25/4, 2013 at 19:6 Comment(2)
I think using test_request_context in a non-test environment is not a pretty good idea.Croatia
This solved a problem when using Flask-Babel in combination with celery. Flask-Babel won't load any translations without a request (because it caches the translations on the request context). Apart from that, Flask-Babel is able to work fine without the request. So using test_request_context() is just a simple way of constructing a functional context, even though it might be a little wasteful.Nolasco
E
0

Without using app.app_context(), just configure the celery before you register blueprints like below :

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

From your blueprint where you wish to use celery, call the instance of celery already created to create your celery task.

It will work as expected.

Enloe answered 24/10, 2019 at 10:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.