cameronmaske / celery-once

Celery Once allows you to prevent multiple execution and queuing of celery tasks.
https://pypi.python.org/pypi/celery_once/
BSD 2-Clause "Simplified" License
661 stars 90 forks source link

With Flask-SQLAlchemy query, I get RuntimeError: Working outside of application context. #64

Closed pdoggi closed 5 years ago

pdoggi commented 6 years ago

Thank you for the great work on this library!

Info Details
OS name and version macOS High Sierra v10.13.3
Broker RabbitMQ
Other details RedBeat, Flask-SQLAlchemy

Following the usage directions worked perfectly out of the box, but once I needed to lookup a record in my database, I get a RuntimeError: Working outside of application context.

This code works

@celery.task(base=QueueOnce, once={'graceful': True})
def slow_task(id):
    sleep(20)
    return "Done!"

This code doesn't work

@celery.task(base=QueueOnce, once={'graceful': True})
def slow_task(id):
    result = CustomORModel.query.filter_by(id=id).first()
    do_something_with(result)
    return "Fails!!"

Using celery's base Task class plays nicely with my db lookup--that is, it works when I remove base=QueueOnce, once={'graceful': True}, but I need the task locking mechanism offered by celery-once.

Anyone else have experience with this? Maybe, I'm missing something pretty basic in my setup or the way I'm calling my query.

Any help would be greatly appreciated! Thanks in advance.

Connie

cameronmaske commented 6 years ago

Hey pdoggi, your setup looks correct.

As a work around, maybe you could force the app context with something like this...

app = Flask(__name__) # If declared in same file, or import it

@celery.task(base=QueueOnce, once={'graceful': True})
def slow_task(id):
    with app.app_context():
        result = CustomORModel.query.filter_by(id=id).first()
        do_something_with(result)

Could you post the full stack-trace and which version of flask this is with so I can debug this a little further?

daoluan commented 6 years ago

very strange for this problem.

mbierma commented 5 years ago

In the Flask documentation, they give the following example for Celery integration:


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)

    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    celery.Task = ContextTask
    return celery

In order to get QueueOnce to properly inherit from ContextClass instead of celery.Task, the following snippet will work:

ContextQueueOnce = QueueOnce
ContextQueueOnce.__bases__ = (ContextTask,)

This is not recommended, but works for the current version

cameronmaske commented 5 years ago

@mbierma Thanks for the work-around!

I'm gonna update documentation with an alternative, with a context aware QueueOnce task base. If anyone can figure out a more elegant solution, it would be welcome.

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)

    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    class ContextQueueOnce(QueueOnce):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return super(ContextQueueOnce, self).__call__(*args, **kwargs)

    celery.Task = ContextTask
    celery.QueueOnce = ContextQueueOnce
    return celery

...
celery = make_celery(app)
@celery.task(base=celery.QueueOnce)
def example_task(value):
    return 
cameronmaske commented 5 years ago

Gonna close this out, as it's added to the docs! If anyone figures out a more elegant way to integrate celery_once with Flask, happy to try and put that in place instead.