Rubyの動かないコード (中級編) Ruby on Railsで,スレッドごとにトランザクションを分離したい
以下のRuby on Railsのコードが,意図した動作をしないのはなぜですか。(制限時間1分)
やりたい事
- 2つのスレッドを同時に動かす。
- スレッド1では,レコードを登録後,ロールバックする。
- スレッド2では,レコードを登録後,コミットする。
タイミング順に,発生させたい出来事:
- スレッド1をスタート
- ↓ スレッド2をスタート
- スレッド1でトランザクション開始 ↓
- ↓ スレッド2でトランザクション開始
- テーブル1でレコード作成 ↓
- ↓ テーブル2でレコード作成
- スレッド1でトランザクションをロールバック ↓
- ↓ スレッド2でトランザクションをコミット
- スレッド1を終了 ↓
- スレッド2を終了
※それぞれ,「↓」と書いてある部分で1秒待機する。
# メイン処理を実行するためのダミークラス class Hoge < ActiveRecord::Base def self.main # 1個目のスレッドを開始 t1 = Thread.start{ sleep 1 begin # トランザクションを開始 Table1.transaction do sleep 1 # レコードを登録する Table1.create({ :name => "レコード1" }) sleep 1 # 例外を発生させ,トランザクションをロールバックする raise end rescue # end sleep 1 # スレッドを終了する } # 次のスレッドを開始するまでに1秒置く sleep 1 # 2個目のスレッドを開始 t2 = Thread.start{ sleep 1 # トランザクションを開始 Table2.transaction do sleep 1 # レコードを登録する Table2.create({ :name => "レコード2" }) sleep 1 # トランザクションをコミットする end sleep 1 # スレッドを終了する } # 2つのスレッドが終了するまで待つ t1.join t2.join # それぞれのテーブルに登録されたデータを調べる p Table1.find_all p Table2.find_all end end
注:
- Thread.startのブロック内は,新規スレッドで実行される。
- スレッドに .join すると,そのスレッドの終了まで待機する。
実行:
ruby script/console >Hoge.main
期待する結果:
Table1にはデータが登録されておらず,
Table2には「レコード2」というデータが登録されていて欲しい。
実際の結果:
[] []
Table1にも,Table2にも,データは登録されない。
また,警告メッセージが2つ出る。
- 3: スレッド1でトランザクション開始
- 4: スレッド2でトランザクション開始
この4の時点で
WARNING: there is already a transaction in progress
という警告が出る。
また,
- 7: スレッド1でトランザクションをロールバック
- 8: スレッド2でトランザクションをコミット
この8の時点で
WARNING: there is no transaction in progress.
という警告が出る。
スレッドごとにトランザクションが分離されていない。
スレッド1でロールバックが発生すると,
スレッド2もロールバックされてしまう。
スレッド間で,トランザクションを排他的にするにはどうしたらよいのか。
解説策
スレッドごとにトランザクションを分離するためには,mainの最初の1行目に
ActiveRecord::Base.allow_concurrency = true
とすればよい。
並列処理でActiveRecordを使う
http://d.hatena.ne.jp/taslam/20080730/p1処理は並列化できたが、これだとDBへのコネクションは1つしかなく、例えばスレッド毎にトランザクションを開始するようなことはできない。
ActiveRecordのコードを追ってみたところ、どうやら
ActiveRecord::Base.allow_concurrency = true
とすれば良いようだ。
こうすることで、スレッド毎にコネクションを保持するようになり、スレッド毎に別々のトランザクションを開始することができる。
これで実行しなおせば,レコード1の登録のみがロールバックされる。
レコード2の登録はロールバックされず,独立してコミットされる。