こんにちは、鈴木です。
バッチ処理を作成する時に、気を付けなければならないことの一つに、排他制御があります。
排他制御を行なう方法はいくつかありますが、今回はロックファイルによる排他制御を行なうコードを考えます。
排他制御を忘れると
排他制御を忘れていると、
- cron で定期的にパッチ処理を起動するように設定した。
- 前回起動されたバッチ処理がまだ終了していなかったので重複して起動された。
- そんな状況は考慮していなかったので、バッチ処理中にエラーが発生した。データ不整合が発生した。
といったことになりかねません。
ロックファイルによる排他制御
ロックファイルによる排他制御とは、以下のような手順で排他的に処理を実行する方法のことを言います。
- バッチ処理の最初にファイルをロックする。( File#flock() を使用します )
- ロックに失敗したら、処理を終了する。( or ロックが取得できるまで待機する )
- 本来の処理を実行する。
- ロックを開放する。
そして、ロックをかける対象のファイルを「ロックファイル」と呼ぶことが多いです。
それではさっそく、ロックファイルによる排他制御を行なうサンプルコードを書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# coding: utf-8 File.open('lock', 'w') do |lock_file| if lock_file.flock(File::LOCK_EX|File::LOCK_NB) # それなりに時間のかかる処理として, 1 秒間隔で 'Hello' と 3 回表示する. 3.times do puts 'Hello' sleep 1 end else puts '他のプロセスが実行中です' end end |
上のコードをファイルに保存して実行、処理が終わる前にもう一回実行、とすれば排他制御が行なわれることを確認できます。
Linux であれば、上記コードを lock_file_sample.rb というファイル名で保存し、以下のように実行( & を付けてバックグラウンドで実行)すると、次のような動作をするはずです。
1 2 3 4 5 6 |
> ruby lock_file_sample.rb & Hello > ruby lock_file_sample.rb & 他のプロセスが実行中です Hello Hello |
ブロックを排他的に実行するメソッドにまとめる
ロックファイルによる排他制御の方法は分かったので、次は使いやすいようにメソッドにまとめようと思います。
Ruby といえばブロック、ということで「指定されたブロックを排他的に実行するメソッド」を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# # ファイルロックに失敗したことを表す例外. # class Locked < StandardError end # # 指定されたブロックを排他的に実行する. # # ==== 詳細 # ロックファイルによる排他制御を行なう. # # ==== 引数 # lock_file_path:: ロックファイルのパス. # def synchronized(lock_file_path='lock') File.open(lock_file_path, 'w') do |lock_file| if lock_file.flock(File::LOCK_EX|File::LOCK_NB) yield else raise Locked end end end |
メソッド名は synchronized としました。また、ファイルのロックに失敗したことを表す例外クラス Locked を定義しました。
synchronized メソッドを使用すると、以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 |
begin synchronized do # それなりに時間のかかる処理として, 1 秒間隔で 'Hello' と 3 回表示する. 3.times do puts 'Hello' sleep 1 end end rescue Locked puts '他のプロセスが実行中です' end |
排他制御を行なうコードと、排他的に実行したいコードを分離することができました。
Rails でバッチ処理を書く
ロックファイルで排他制御する方法は、Rails でバッチ処理を書く場合にも使用することができます。
バッチ処理は複数作成する場合もあるので、Batch::Base クラスに排他制御などの共通機能をまとめます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# # 他のプロセスにより処理が実行中であることを表す例外. # class Batch::Locked < StandardError end # # バッチ処理の基本クラス. # # ==== 詳細 # 派生クラスは以下のようにインスタンスメソッド execute を定義する. # 派生クラスで定義された execute は排他的に実行される. # # class Batch::Hello < Batch::Base # def execute # ... # end # end # # バッチ処理は以下のように起動する. # # rails runner 'Batch::Hello.execute' # class Batch::Base def self.execute(*arguments) synchronized do new.execute(*arguments) end rescue Batch::Locked puts '他のプロセスが実行中のため処理をスキップします' end def self.synchronized File.open(lock_file_path, 'w') do |lock_file| if lock_file.flock(File::LOCK_EX|File::LOCK_NB) yield else raise Batch::Locked, "#{lock_file.path} is locked." end end end def self.lock_file_path Rails.root.join('tmp', "#{self.name.underscore.gsub('/', '.')}.lock") end end |
Batch::Base を継承し、execute というインスタンスメソッドを定義します。
1 2 3 4 5 6 7 8 9 10 |
class Batch::Hello < Batch::Base def execute 3.times do puts 'Hello' sleep 1 end end end |
バッチ処理は以下のように起動します。
1 |
rails runner 'Batch::Hello.execute' |
バッチ処理を起動すると、最初に Batch::Base で定義されたクラスメソッドの execute が呼び出されます。
Batch::Base.execute では synchronized の中で派生クラスで定義されてるインスタンスメソッドの execute を呼び出します。
まとめ
今回はロックファイルで排他制御する、というアプローチをご紹介しました。
単純に排他制御する方法に加えて、Rails でバッチ処理を作成する方法についてもお話ししました。
こんなやり方もあるんだ、と思っていただければ幸いです。