web-dev-qa-db-fra.com

Comment utiliser Flask-SQLAlchemy dans une tâche Celery

Je suis récemment passé à Celery 3.0. Avant cela, j'utilisais Flask-Celery afin d'intégrer Celery avec Flask. Bien qu'il ait eu de nombreux problèmes comme le masquage de certaines fonctionnalités puissantes de Celery, il m'a permis d'utiliser le contexte complet de l'application Flask et surtout Flask-SQLAlchemy.

Dans mes tâches d'arrière-plan, je traite des données et l'ORM SQLAlchemy pour stocker les données. Le mainteneur de Flask-Celery a abandonné le support du plugin. Le plugin décapait l'instance Flask dans la tâche afin que je puisse avoir un accès complet à SQLAlchemy.

J'essaie de reproduire ce comportement dans mon fichier tasks.py mais sans succès. Avez-vous des conseils sur la façon d'y parvenir?

39
PanosJee

Mise à jour: nous avons depuis commencé à utiliser une meilleure façon de gérer le démontage et la configuration des applications par tâche, sur la base du modèle décrit dans la documentation la plus récente flask .

extensions.py

import flask
from flask.ext.sqlalchemy import SQLAlchemy
from celery import Celery

class FlaskCelery(Celery):

    def __init__(self, *args, **kwargs):

        super(FlaskCelery, self).__init__(*args, **kwargs)
        self.patch_task()

        if 'app' in kwargs:
            self.init_app(kwargs['app'])

    def patch_task(self):
        TaskBase = self.Task
        _celery = self

        class ContextTask(TaskBase):
            abstract = True

            def __call__(self, *args, **kwargs):
                if flask.has_app_context():
                    return TaskBase.__call__(self, *args, **kwargs)
                else:
                    with _celery.app.app_context():
                        return TaskBase.__call__(self, *args, **kwargs)

        self.Task = ContextTask

    def init_app(self, app):
        self.app = app
        self.config_from_object(app.config)


celery = FlaskCelery()
db = SQLAlchemy()

app.py

from flask import Flask
from extensions import celery, db

def create_app():
    app = Flask()

    #configure/initialize all your extensions
    db.init_app(app)
    celery.init_app(app)

    return app

Une fois que vous avez configuré votre application de cette façon, vous pouvez exécuter et utiliser le céleri sans avoir à l'exécuter explicitement à partir d'un contexte d'application, car toutes vos tâches seront automatiquement exécutées dans un contexte d'application si nécessaire, et vous n'avez pas pour vous préoccuper explicitement du démontage post-tâche, qui est un problème important à gérer (voir les autres réponses ci-dessous).

Dépannage

Ceux qui continuent à obtenir with _celery.app.app_context(): AttributeError: 'FlaskCelery' object has no attribute 'app' s'assurent de:

1) Conservez l'importation celery au niveau du fichier app.py. Éviter:

app.py

from flask import Flask

def create_app():
    app = Flask()

    initiliaze_extensions(app)

    return app

def initiliaze_extensions(app):
    from extensions import celery, db # DOOMED! Keep celery import at the FILE level

    db.init_app(app)
    celery.init_app(app)

2) Commencez par travailler le céleri AVANT vous flask run Et utilisez

celery worker -A app:celery -l info -f celery.log

Notez le app:celery, C'est-à-dire le chargement à partir de app.py.

Vous pouvez toujours importer des extensions pour décorer des tâches, c'est-à-dire from extensions import celery.

Ancienne réponse ci-dessous, fonctionne toujours, mais pas une solution aussi propre

Je préfère exécuter tout le céleri dans le contexte de l'application en créant un fichier séparé qui invoque celery.start () avec le contexte de l'application. Cela signifie que votre fichier de tâches ne doit pas être jonché de configuration de contexte et de démontages. Il se prête également bien au modèle flask 'fabrique d'application'.

extensions.py

from from flask.ext.sqlalchemy import SQLAlchemy
from celery import Celery

db = SQLAlchemy()
celery = Celery()

tasks.py

from extensions import celery, db
from flask.globals import current_app
from celery.signals import task_postrun

@celery.task
def do_some_stuff():
    current_app.logger.info("I have the application context")
    #you can now use the db object from extensions

@task_postrun.connect
def close_session(*args, **kwargs):
    # Flask SQLAlchemy will automatically create new sessions for you from 
    # a scoped session factory, given that we are maintaining the same app
    # context, this ensures tasks have a fresh session (e.g. session errors 
    # won't propagate across tasks)
    db.session.remove()

app.py

from extensions import celery, db

def create_app():
    app = Flask()

    #configure/initialize all your extensions
    db.init_app(app)
    celery.config_from_object(app.config)

    return app

RunCelry.py

from app import create_app
from extensions import celery

app = create_app()

if __name__ == '__main__':
    with app.app_context():
        celery.start()
66
Paul Gibbs

Dans votre fichier tasks.py, procédez comme suit:

from main import create_app
app = create_app()

celery = Celery(__name__)
celery.add_defaults(lambda: app.config)

@celery.task
def create_facet(project_id, **kwargs):
    with app.test_request_context():
       # your code
5
PanosJee

J'ai utilisé réponse de Paul Gibbs avec deux différences. Au lieu de task_postrun, j'ai utilisé worker_process_init. Et au lieu de .remove (), j'ai utilisé db.session.expire_all ().

Je ne suis pas sûr à 100%, mais d'après ce que je comprends de la façon dont cela fonctionne, lorsque Celery crée un processus de travail, toutes les sessions db héritées/partagées seront expirées et SQLAlchemy créera de nouvelles sessions à la demande propres à ce processus de travail.

Jusqu'à présent, il semble avoir résolu mon problème. Avec la solution de Paul, lorsqu'un travailleur a terminé et supprimé la session, un autre travailleur utilisant la même session exécutait toujours sa requête. serveur pendant la requête "exception.

Merci Paul de m'avoir dirigé dans la bonne direction!

Peu importe, cela n'a pas fonctionné. J'ai fini par avoir un argument dans mon Flask fabrique d'applications pour ne pas exécuter db.init_app (app) si Celery l'appelait. Au lieu de cela, les travailleurs l'appelleront après que Celery les aura forks. Je vois maintenant plusieurs connexions dans ma liste de processus MySQL.

from extensions import db
from celery.signals import worker_process_init
from flask import current_app

@worker_process_init.connect
def celery_worker_init_db(**_):
    db.init_app(current_app)
5
Robpol86
from flask import Flask
from werkzeug.utils import import_string
from celery.signals import worker_process_init, celeryd_init
from flask_celery import Celery
from src.app import config_from_env, create_app

celery = Celery()

def get_celery_conf():
    config = import_string('src.settings')
    config = {k: getattr(config, k) for k in dir(config) if k.isupper()}
    config['BROKER_URL'] = config['CELERY_BROKER_URL']
    return config

@celeryd_init.connect
def init_celeryd(conf=None, **kwargs):
    conf.update(get_celery_conf())

@worker_process_init.connect
def init_celery_flask_app(**kwargs):
    app = create_app()
    app.app_context().Push()
  • Mettre à jour la configuration de céleri sur Celeryd Init
  • Utilisez votre flask fabrique d'applications pour initialiser toutes les extensions flask, y compris l'extension SQLAlchemy).

Ce faisant, nous pouvons maintenir la connexion à la base de données par travailleur.

Si vous souhaitez exécuter votre tâche sous le contexte flask, vous pouvez sous-classer Task.__call__:

class SmartTask(Task):

    abstract = True

    def __call__(self, *_args, **_kwargs):
        with self.app.flask_app.app_context():
            with self.app.flask_app.test_request_context():
                result = super(SmartTask, self).__call__(*_args, **_kwargs)
            return result

class SmartCelery(Celery):

    def init_app(self, app):
        super(SmartCelery, self).init_app(app)
        self.Task = SmartTask
1
soasme