SQLAlchemy との戦い

ここ数ヶ月 SQLAlchemy を使って開発をしている。


開発当初は特に問題もなく調子よく行っていたが、自分のローカル環境の MySQL の設定を本番に近づけたため、SQLAlchemy がエラーをはいた - Memo が出た。
pool_recycle の値を短くすれば解決と思ったが、解決しなかった。

前提として FW は Flask を利用。SQLAlchemy は素で scoped_session を使っている。
Flask-SQLAlchey や Flask-Alchemy-Session は使ってない。
autocommit, autoflush は False の設定。


でたエラーが

sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (2013, 'Lost connection to MySQL server during query') 

だったり

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) Can't reconnect until invalid transaction is rolled back 

だったり。


現象としては、一つのページにとどまっていて、wait_timeout で設定した 60 秒を超えて、ページをリロード(MySQL にアクセス)した際にでる。
# 更新系は出ない


とりあえずぐぐって、出てきたページを片っ端から試してみた。
SQLAlchemyの OperationalError: MySQL Connection not available エラー - Life is Really Short, Have Your Life!!
コメント欄にあるように、session.remove() をリクエストの最後に呼んでやる。
これは元々 Flask の teardown_request で呼んでいる。
効果無し。


FlaskとSQLAlchemyを使っててMySQL server has gone awayってなる - petitviolet_blog
SQLAlchemy+MySQLで「Lost connection to MySQL server during query」|python|サムライファクトリー開発ブログ
にあるように、Pool event を使って、MySQL リクエストごとに ping を飛ばす。
Connection Pooling — SQLAlchemy 1.3 Documentation
効果無し。


http://perezvon.hatenablog.com/entry/20071215/1197702294
@soundkitchen さんに教えて貰った。
リクエストの最後は漏れる可能性があるから、リクエストの最初に remove() してセッションを作り直してみたら? というもの。
効果なし。


更新系ではでなくて、参照系で出るのはなにか違いがあるのかと思って、コードを見直したら、session.commit() しているかしていないかの違いがあった。
これかと思って、試してみたら、正解だった。


これで万事解決したかと思ったが、依然でる所があった。
因みにでたのが、WTForms で SQLAlchemy からセレクトボックスに DB の項目を出している所だった。


こんな感じで、セッションのクローズ漏れが出てくるのが怖い。
SQLAlchemy — The Pyramid Community Cookbook v0.2
Pyramid のドキュメントではリクエストコンテキストの最後に、必ず commit, close をしていた。
これにならって、以下のようにした。

@app.teardown_appcontext
def session_clear(exception):
    if exception and Session.is_active:
        session.rollback()
    else:
        #session.commit() 最初こうしてたけど、意図して Rollback した際に不具合おきるからなにもしない
        pass
    Session.close()

あと、session 生成時に、session.expire_on_commit = False しておかないと、例外が起きた。

DetachedInstanceError: Instance <MyModel at 0x36bb190> is not bound to a Session;
attribute refresh operation cannot proceed

なので、sessionmaker の際に expire_on_commit を付けるようにした。


とりあえずこれで安定して動くようになった。