テクノロジー · 実用ガイド
Python の「database is locked」:エラーコードと失敗段階から再試行の範囲を決める
タイムアウトを延ばしても古い WAL スナップショットは更新されない。コード、失敗段階、Python のトランザクション設定を記録してから再試行を選ぶ。

同じ database is locked でも、別の書き込み接続を待つ、古い読み取りスナップショットを破棄する、まだ成功していないコミットを完了する、といった異なる復旧が必要になる。最後の UPDATE を無条件に繰り返すと、古い判断を使い続けたり、変更を二度適用したりしかねない。
例外を出した接続から調べる。Python と実行時 SQLite のバージョン、データベースの識別情報、ジャーナルモード、トランザクション設定、BEGIN・UPDATE・COMMIT のどこで失敗したかを記録する。アプリの方針を変える前に、使い捨てのデータベースで再現する。
拡張コードと失敗段階を残す
Python 3.11 以降、SQLite 由来の例外には sqlite_errorcode と sqlite_errorname が付く。getattr(exc, "sqlite_errorcode", None) と getattr(exc, "sqlite_errorname", None) で記録し、完全な値を残してから code & 255 で基本カテゴリを求める。SQLITE_BUSY は 5、SQLITE_LOCKED は 6、SQLITE_BUSY_SNAPSHOT は 517 だ。
BUSY は通常別の接続との競合。LOCKED は同じ接続の未完了文や共有キャッシュに関係することがある。隔離テストでは SELECT の結果を一部だけ読み、カーソルを開いたまま DROP TABLE を実行すると、database table is locked、コード 6 になった。必要な処理後に実際のカーソルを完了・終了させる。文字列だけで無関係なプロセスを終了しない。
トランザクション境界の管理者を確認する
autocommit は Python 3.12 で導入された。autocommit=True なら SQL BEGIN で開始した場合も Connection.commit() と rollback() は何もしない。そのトランザクションは SQL COMMIT または ROLLBACK で終了し、in_transaction を確認する。autocommit=False はコミット・ロールバック直後に新しい DEFERRED トランザクションを開く。isolation_level が制御するのは LEGACY_TRANSACTION_CONTROL だけだ。
実行中のトランザクションで設定変更を修復の近道にしない。autocommit を True に変えると未処理の変更がコミットされる。境界をフレームワークが管理する場合もある。現在の接続コンテキストマネージャーはコミット失敗時にロールバックするが、autocommit=True では何もせず、接続自体も閉じない。2016〜2021 年の解決済み CPython バグを現在も残る問題と扱わない。
待ち時間より先に再試行範囲を選ぶ
処理開始前の BEGIN IMMEDIATE が BUSY なら、上限付きで書き込み権の取得を再試行できる。トランザクションを短くし、その間のネットワーク呼び出しを避ける。BEGIN IMMEDIATE 成功は、すべてのジャーナルモードで後続処理が一切 BUSY にならないという普遍的保証ではない。
別接続のコミット後、WAL の読み取りトランザクションで 517 が出たら、終了して新しく開始し、読み直して判断も再計算する。待機や UPDATE だけの再試行では古いスナップショットは変わらない。ロールバックジャーナルで別の読み手が COMMIT を阻み BUSY になった場合、トランザクションは継続し得る。文書記載の条件が解消したら COMMIT を再試行し、すでに適用した変更を再実行しない。
一時ファイルと二つの接続で観察した結果
Python 3.12.14、実行時 SQLite 3.53.1、timeout=0.05 秒、一時データベース、明示的 SQL 境界で四つの小さな実験を行った。ユーザーのデータベースは使っていない。WAL で A がカウンター 10 を読み、B が 11 に変更し、A の書き込みが 517 で失敗した。autocommit=True の A.rollback() 後も in_transaction=True。SQL ROLLBACK で解除し、新しい BEGIN IMMEDIATE で 11 を読み直して一度増やすと 12 がコミットされた。
一つ目の接続が書き込み権を持つ間、二つ目の BEGIN IMMEDIATE はコード 5 で失敗し、その接続にアクティブなトランザクションはなかった。別の DELETE ジャーナル実験では、開いた読み取りが別接続の COMMIT を阻み、コード 5 でも書き込みトランザクションは続いた。読み取りを終了し COMMIT だけ再試行すると、初期値 10 は 11 になった。再現できる人工的なケースであり、未知のアプリの診断や本番安全性の証明ではない。
上限と観察可能な完了確認を設ける
接続のタイムアウトは待機予算であり、古いスナップショットやデッドロックを修復しない。待っても循環を解消できない場合、SQLite は busy handler を呼ばないことがある。試行回数、経過時間、コード、段階、状態を記録し、上限に達したら失敗を報告する。すべての OperationalError を再試行可能な競合として扱わない。
修復を受け入れる前に、別接続で意図した永続値を確認し、対象トランザクションが終了したか調べ、該当競合を再現する。autocommit=False では新しい空トランザクションにより in_transaction が True のままになり得るので設定と合わせて解釈する。「ロック解除」のため WAL や共有メモリファイルを削除しない。破損、I/O エラー、管理者不明のトランザクションなら、この再試行手順を止めて別原因を調査する。
