私は同じ時折繰り返される厄介なエラーを抱えていました:
OperationalError: (1305, 'SAVEPOINT {{name}} does not exist')
グーグルは、それが一種の「通常の」並行性の問題であることを除いて、それをより明確にしませんでした. そのため、非決定論的であり、開発環境で再現するのは困難です。
幸いなことに、本番アプリのロギングを十分に冗長にすることで、ローカライズすることができました。
原因
MySQL には、暗黙的にトランザクションを終了できる操作がいくつかあります。
- DDL ステートメント (
CREATE TABLE
、ALTER TABLE
など) は、暗黙的なコミットを引き起こします。MySQL の DDL がトランザクション対応でないことはよく知られていますが、
OperationalError: (1213, 'Deadlock found when trying to get lock; try restarting transaction')
OperationalError: (1205, 'Lock wait timeout exceeded; try restarting transaction')
暗黙的なロールバックが発生します。
そのため、2 番目のケースは確かにやや「正常」になります。次のコードで表すことができます。
# db is an example database connection object, which
# - supports nested (stacked) transactions,
# - has autocommit on.
db.begin() # START TRANSACTION
try:
# no-conflict op
db.update()
db.begin() # SAVEPOINT sp1
try:
# conflict op,
# e.g. attempt to change exclusively locked rows by another transaction
db.update()
db.commit() # RELEASE SAVEPOINT sp1
except:
# Everything interesting happens here:
# - the change attempt failed with OperationalError: (1213, 'Deadlock...'),
# - the transaction is rolled back with all the savepoints,
# - next line will attempt to rollback to savepoint which no longer exists,
# - so will raise OperationalError: (1305, 'SAVEPOINT sp1 does not exist'),
# - which will shadow the original exception.
db.rollback() # ROLLBACK TO SAVEPOINT sp1
raise
db.commit() # COMMIT
except:
db.rollback() # ROLLBACK
raise
アップデート
上記の例外シャドーイングは Python 2 について述べたものであることに注意してください。Python 3 は例外チェーンを実装しており、デッドロックが発生した場合、トレースバックにはすべての関連情報が含まれます。