読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Rubyの動かないコード (中級編) Ruby on Railsで,スレッドごとにトランザクションを分離したい

ruby マルチスレッド 動かないコード DB

以下のRuby on Railsのコードが,意図した動作をしないのはなぜですか。(制限時間1分)

やりたい事

  • 2つのスレッドを同時に動かす。
    • スレッド1では,レコードを登録後,ロールバックする。
    • スレッド2では,レコードを登録後,コミットする。


タイミング順に,発生させたい出来事:

  1. スレッド1をスタート
  2. ↓              スレッド2をスタート
  3. スレッド1でトランザクション開始         ↓
  4. ↓              スレッド2でトランザクション開始
  5. テーブル1でレコード作成             ↓
  6. ↓              テーブル2でレコード作成
  7. スレッド1でトランザクションをロールバック  ↓
  8. ↓           スレッド2でトランザクションをコミット
  9. スレッド1を終了                   ↓
  10. スレッド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の登録はロールバックされず,独立してコミットされる。

補足

もし,Ruby on Railsで開発したアプリの「排他制御」(同時アクセスの制御)のテストを書きたい場合,

ここで取り上げたテクニックが必要になる。


DB処理を同時に並行して複数走らせて,わざと処理を「衝突」させ,

それでもDB内に矛盾が発生しないこと,

というのを,単体テストで試験できるわけだ。


もちろんそのためには,排他制御のロジックをコントローラ層(セッションとか使う方法)だけではなく,

モデル層でも完結させる必要がある。(ロックとか使う)