SQLxとSQLiteのライトトランザクションに潜む危険

RustでSQLiteを使う際にSQLxは定番の選択です。しかし、ライトトランザクションには深刻な落とし穴があります。非同期処理と組み合わせるとロック飢餓が発生するのです。そこで今回は、その仕組みと対策を解説します。

SQLiteの書き込みロックの基本

SQLiteはシングルライターモデルです。WALモードでも書き込みは常に1つだけです。つまり、同時に書き込めるのは1つのトランザクションだけです。しかし、読み取りは複数同時に可能です。

具体的には、書き込み前にEXCLUSIVEロックを取得します。また、他の書き込みはロック解放まで待機します。さらに、デフォルトのタイムアウトは5秒です。そのため、長時間のトランザクションは他の処理をブロックします。実際、これ自体は単純な仕組みです。

非同期処理でロック飢餓が起きる仕組み

問題はSQLxの非同期処理との組み合わせで発生します。SQLxはコネクションプールを使います。しかし、非同期タスクはawaitの度にコネクションを手放す可能性があります。つまり、トランザクション中に他のタスクが割り込む隙が生まれます。

たとえば、タスクAがライトロックを取得中にawaitします。するとタスクBが別のコネクションでライトを試みます。しかし、タスクAのロックが残っているため失敗します。さらに、タスクが増えるほど競合が激化します。そのため、特定のタスクが永遠にロックを取得できない飢餓状態に陥ります。

具体的な対策方法

対策はいくつかあります。まず、書き込み用のコネクションを1つに限定する方法です。つまり、プールサイズを書き込み用は1にします。そのため、ロックの競合自体が発生しなくなります。

また、BEGIN IMMEDIATEを使う方法もあります。具体的には、トランザクション開始時にロックを即座に取得します。さらに、busy_timeoutを適切に設定することも重要です。実際、これらの組み合わせでロック飢餓を防止できます。特に、コネクションプールの設計が最も効果的な対策です。

SQLx以外のライブラリでの状況

しかし、この問題はSQLx固有ではありません。たとえば、rusqliteでもasync-stdと組み合わせると同様の問題が起きます。なお、同期的なアクセスならこの問題は発生しません。つまり、非同期とSQLiteの組み合わせ自体が注意を要するのです。

とはいえ、Rustの型システムがある程度の安全性を提供します。また、tokioのspawn_blockingで同期的に処理する方法もあります。そのため、設計段階での対策が重要です。このように、ツールの特性を理解した上での実装が求められます。

まとめ

SQLxとSQLiteの非同期ライトトランザクションにはロック飢餓のリスクがあります。しかし、書き込みコネクションの限定やBEGIN IMMEDIATEで対策可能です。特に、非同期処理とSQLiteを組み合わせる際は設計段階での考慮が不可欠です。