web-dev-qa-db-ja.com

リクエスト間で持続する無効なトランザクション

概要

本番環境のスレッドの1つがエラーになり、それが存続するクエリを含むすべてのリクエストで_InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction._エラーを生成しています。 daysでこれを実行しています。これはどのようにして可能であり、どうすればそれを防ぐことができますか?

バックグラウンド

Flask app on uWSGI(4プロセス、2スレッド))を使用しており、Flask-SQLAlchemyはSQL ServerへのDB接続を提供します。

このFlask-SQLAlchemyメソッド内で、本番環境のスレッドの1つがリクエストを破棄したときに問題が発生したようです。

_@teardown
def shutdown_session(response_or_exc):
    if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
        if response_or_exc is None:
            self.session.commit()
    self.session.remove()
    return response_or_exc
_

...そしてなんとかトランザクションが無効なときにself.session.commit()を呼び出すことができました。これにより、ロギング構成に反して、_sqlalchemy.exc.InvalidRequestError: Can't reconnect until invalid transaction is rolled back_がstdoutに出力されるようになりました。これは、例外を発生させることが想定されていないアプリコンテキストの破棄中に発生したため、理にかなっています。よくわかりませんhow _response_or_exec_が設定されていない場合、トランザクションは無効になりますが、それは実際にはAFAIKの方が問題が少ないことです。

より大きな問題は、「準備された」状態のエラーが発生し、それ以降停止していないことです。このスレッドがDBにヒットするリクエストを処理するたびに、500秒になります。他のすべてのスレッドは問題ないようです。私が知る限り、同じプロセスにあるスレッドでも問題ありません。

当てずっぽう

SQLAlchemyメーリングリストには、セッションがコミットを開始してまだ完了していない場合に発生し、他の何かがそれを使用しようとした場合に発生する「準備済み」エラーに関するエントリがあります。私の推測では、このスレッドのセッションはself.session.remove()ステップに到達したことはなく、今は到達しません。

それでも、このセッションがどのように永続化されているかは説明されていませんリクエスト全体がFlask-SQLAlchemyのリクエストスコープのセッションの使用を変更していないため、エラーが発生したセッションも含めて、セッションはSQLAlchemyのプールに返され、リクエストの最後にロールバックされるはずです(確かに、おそらく最初のセッションではありませんが、アプリコンテキストの破棄中に発生したため)。ロールバックが発生しないのはなぜですか? stdout(uwsgiのログ)に「無効なトランザクション」エラーが毎回表示されていれば理解できましたが、そうではありません。初めて表示されたのは1回だけです。しかし、500が発生するたびに(アプリのログに)「準備済み」状態のエラーが表示されます。

構成の詳細

_expire_on_commit_で_session_options_をオフにし、_SQLALCHEMY_COMMIT_ON_TEARDOWN_をオンにしました。データベースから読み取るだけで、まだ書いていません。また、すべてのクエリにDogpile-Cacheを使用しています(複数のプロセスがあり、実際には2つの負荷分散サーバーがあるため、memcachedロックを使用しています)。主要なクエリでは、キャッシュは1分ごとに期限切れになります。

2014年4月28日更新:解決手順

サーバーを再起動することで問題が解決したようですが、これはまったく驚くことではありません。とはいえ、止める方法がわかるまではまた見たいと思います。 benselme(下記)は、コミットに関する例外処理を使用して独自のティアダウンコールバックを記述することを提案しましたが、より大きな問題は、スレッドが残りの期間にわたってめちゃくちゃになったことです。これされなかったリクエストの後に消えるという事実は、本当に緊張します!

37
Vanessa Phipps

2016年6月5日を編集:

この問題を解決するPRが2016年5月26日に統合されました。

Flask PR 1822

2015年4月13日を編集:

謎解きました!

TL; DR:Be絶対に確認してくださいティアダウン関数は、2014-12-11編集でティアダウンラッピングレシピを使用して成功します!

Flaskを使用して新しいジョブも開始しましたが、ティアダウンラッピングレシピを導入する前に、この問題が再び発生しました。そこで私はこの問題を再検討し、最終的に何が起こったのかを理解しました。

私が思ったように、Flaskは、新しい要求がラインを下りるたびに新しい要求コンテキストを要求コンテキストスタックにプッシュします。これは、セッションのような要求ローカルグローバルをサポートするために使用されます。

Flaskには、リクエストコンテキストとは別の「アプリケーション」コンテキストの概念もあります。これは、HTTPが発生していないテストやCLIアクセスなどをサポートするためのものです。私はこれを知っていて、Flask-SQLAがそのDBセッションを配置する場所であることも知っていました。

通常の操作では、リクエストとアプリコンテキストの両方がリクエストの最初にプッシュされ、最後にポップされます。

ただし、リクエストコンテキストをプッシュすると、リクエストコンテキストは既存のアプリコンテキストがあるかどうかを確認し、存在する場合はしないことがわかります新しいものをプッシュします!

したがって、ティアダウン関数が発生したためにリクエストの最後にアプリコンテキストがポップされない場合、それは永久にとどまるだけでなく、新しいその上にプッシュされたアプリコンテキスト。

これは、統合テストで理解できなかった魔法についても説明しています。テストデータを挿入してからリクエストを実行すると、コミットしていなくても、それらのリクエストはそのデータにアクセスできます。これは、リクエストに新しいリクエストコンテキストがあるためにのみ可能ですが、テストアプリケーションコンテキストを再利用しているため、既存のDB接続を再利用しています。つまり、これはバグではなく機能です。

とはいえ、以下の分解関数ラッパーのようなものを使用して、分解関数が確実に成功する必要があることを意味します。これは、メモリとDB接続のリークを回避するために、その機能がなくても良い考えですが、これらの結果に照らして特に重要です。このため、FlaskのドキュメントにPRを送信します。 ( ここにあります

2014-12-11を編集:

最終的に配置したのは、次のコード(アプリケーションファクトリ内)です。このコードは、すべてのティアダウン関数をラップして、例外がログに記録され、それ以上発生しないことを確認します。これにより、アプリのコンテキストが常に正常にポップされます。明らかに、これはに移動する必要があります。すべてのティアダウン関数が登録されていることを確認します。

_# Flask specifies that teardown functions should not raise.
# However, they might not have their own error handling,
# so we wrap them here to log any errors and prevent errors from
# propagating.
def wrap_teardown_func(teardown_func):
    @wraps(teardown_func)
    def log_teardown_error(*args, **kwargs):
        try:
            teardown_func(*args, **kwargs)
        except Exception as exc:
            app.logger.exception(exc)
    return log_teardown_error

if app.teardown_request_funcs:
    for bp, func_list in app.teardown_request_funcs.items():
        for i, func in enumerate(func_list):
            app.teardown_request_funcs[bp][i] = wrap_teardown_func(func)
if app.teardown_appcontext_funcs:
    for i, func in enumerate(app.teardown_appcontext_funcs):
        app.teardown_appcontext_funcs[i] = wrap_teardown_func(func)
_

2014-09-19を編集:

1)複数のスレッドを使用していて、2)リクエストの途中でスレッドを終了すると問題が発生する可能性がある場合、_--reload-on-exception_は良い考えではありません。 uWSGIの「グレースフルリロード」機能のように、uWSGIはそのワーカーに対するすべてのリクエストが完了するのを待つと思っていましたが、そうではありません。 Memcachedでスレッドがドッグパイルロックを取得し、別のスレッドの例外のためにuWSGIがワーカーをリロードすると、ロックが解放されないという問題が発生し始めました。

_SQLALCHEMY_COMMIT_ON_TEARDOWN_を削除すると、問題の一部が解決されますが、アプリのティアダウン中に時々エラーが発生しますduringsession.remove()。これらは SQLAlchemyの問題304 によって引き起こされているようです。バージョン0.9.5で修正されたため、うまくいけば、0.9.5にアップグレードすると、常に機能しているアプリコンテキストの破棄に依存できるようになります。

元:

これが最初にどのように発生したかは未だに未解決の問題ですが、私はそれを防ぐ方法を見つけました:uWSGIの_--reload-on-exception_オプション。

私たちのFlaskアプリのエラー処理は、ほぼすべてをキャッチする必要があるため、カスタムエラー応答を提供できます。つまり、最も予期しない例外だけがuWSGIに到達するはずです。したがって、それが発生するたびにアプリ全体をリロードすることを意味します。

_SQLALCHEMY_COMMIT_ON_TEARDOWN_もオフにしますが、データベースへの書き込みはほとんどないため、アプリティアダウン用の独自のコールバックを作成するのではなく、明示的にコミットする可能性があります。

33
Vanessa Phipps

驚くべきことは、その周りに例外処理がないことですself.session.commit。また、たとえばDBへの接続が失われた場合、コミットが失敗する可能性があります。そのため、コミットは失敗し、sessionは削除されず、次に特定のスレッドが要求を処理するときに、今は無効なセッションを使用しようとします。

残念ながら、Flask-SQLAlchemyは独自の分解関数を持つための明確な可能性を提供しません。 1つの方法は、SQLALCHEMY_COMMIT_ON_TEARDOWN Falseに設定してから、独自の分解関数を記述します。

次のようになります。

@app.teardown_appcontext
def shutdown_session(response_or_exc):
    try: 
        if response_or_exc is None:
            sqla.session.commit()
    finally:
        sqla.session.remove()
    return response_or_exc

これで、失敗したコミットが残り、個別に調査する必要があります...しかし、少なくともスレッドは回復するはずです。

6
benselme