web-dev-qa-db-ja.com

CeleryタスクでFlask-SQLAlchemyを使用する方法

最近、Celery 3.0に切り替えました。その前は、CeleryをFlaskと統合するために Flask-Celery を使用していました。いくつかの強力なセロリ機能を隠すなど多くの問題がありましたが、Flaskアプリ、特にFlask-SQLAlchemyの完全なコンテキストを使用することができました。

私のバックグラウンドタスクでは、データとデータを格納するSQLAlchemy ORMを処理しています。 Flask-Celeryのメンテナはプラグインのサポートをやめました。プラグインはタスクのFlaskインスタンスを酸洗いしていたので、SQLAlchemyへの完全なアクセス権を持つことができました。

この動作をtasks.pyファイルに再現しようとしていますが、成功しません。これを達成する方法についてのヒントはありますか?

39
PanosJee

更新:私たちは、アプリケーションのティアダウンを処理するためのより良い方法を使い始め、記述されたパターンに基づいて、タスクごとに設定し始めました 最新の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
_

この方法でアプリを設定すると、アプリケーションコンテキスト内から明示的に実行する必要なく、セロリを実行して使用できます。すべてのタスクは、必要に応じてアプリケーションコンテキストで自動的に実行されます。管理する重要な問題である、タスク後の分解について明示的に心配する(以下の他の応答を参照)。

トラブルシューティング

with _celery.app.app_context(): AttributeError: 'FlaskCelery' object has no attribute 'app'を取得し続ける人は、次のことを確認してください。

1)celeryインポートを_app.py_ファイルレベルで維持します。避ける:

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)_flask run_の前にセロリワーカーを開始して使用する

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

_app:celery_、つまり_app.py_からのロードに注意してください。

拡張機能からインポートしてタスクを装飾することもできます。つまり、_from extensions import celery_です。

以下の古い答えはまだ機能しますが、解決策はそれほどきれいではありません

アプリケーションのコンテキストでcelery.start()を呼び出す別のファイルを作成して、アプリケーションコンテキスト内ですべてのceleryを実行することを好みます。これは、タスクファイルをコンテキストのセットアップや破棄で散らかす必要がないことを意味します。また、flask 'application factory'パターンに適しています。

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
_

RunCelery.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

Tasks.pyファイルで、次の操作を行います。

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

私は Paul Gibbsの回答 を使用しましたが、2つの違いがあります。 task_postrunの代わりに、worker_process_initを使用しました。そして、.remove()の代わりに、db.session.expire_all()を使用しました。

100%確信はありませんが、これが機能する方法を理解しているところによると、Celeryがワーカープロセスを作成すると、継承/共有されたすべてのdbセッションが期限切れになり、SQLAlchemyはそのワーカープロセスに固有のオンデマンドで新しいセッションを作成します。

これまでのところ、問題は解決したようです。 Paulのソリューションでは、あるワーカーがセッションを終了して削除したときに、同じセッションを使用している別のワーカーがまだクエリを実行していたため、db.session.remove()が使用中に接続を閉じ、「MySQLへの接続が失われましたクエリ中のサーバー」例外。

正しい方向に導いてくれたPaulに感謝します。

うまくいきませんでした。 Flaskアプリファクトリで、Celeryがそれを呼び出している場合、db.init_app(app)を実行しないように引数を指定しました。代わりに、Celeryがフォークした後、ワーカーがそれを呼び出します。 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()
  • Celeryd initでcelery configを更新する
  • flaskアプリファクトリを使用して、SQLAlchemy拡張を含むすべてのflask拡張を初期化します。

これにより、ワーカーごとのデータベース接続を維持できます。

flaskコンテキストでタスクを実行する場合は、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