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?
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).
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
.
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()
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
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)
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()
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