熟考した後、これを ActiveRecord 固有のものにすることにしました。ruby のensure
機能と ActiveRecord のdestroyed?
andchanged?
メソッドを利用することで、設計はシンプルになります。
:name と :state を使用してチェックポイント モデルを定義する
# file db/migrate/xyzzy_create_checkpoints.rb
class CreateCheckpoints < ActiveRecord::Migration
def change
create_table :checkpoints do |t|
t.string :name
t.string :state
end
add_index :checkpoints, :name, :unique => true
end
end
# file app/models/checkpoint.rb
class Checkpoint < ActiveRecord::Base
serialize :state
end
WithCheckpoint モジュールを定義する
# file lib/with_checkpoint.rb
module WithCheckpoint
def with_checkpoint(name, initial_state, &body)
r = Checkpoint.where(:name => name)
# fetch existing or create fresh checkpoint
checkpoint = r.exists? ? r.first : r.new(:state => initial_state)
begin
yield(checkpoint)
ensure
# upon leaving the body, save the checkpoint iff needed
checkpoint.save if (!(checkpoint.destroyed?) && checkpoint.changed?)
end
end
end
使用例
これは、何回かの繰り返しの後にランダムに爆発するやや不自然な例です。より一般的なケースは、いずれかの時点で失敗する可能性のある長時間のネットワークまたはファイル アクセスである可能性があります。注: 「状態」が単純な整数である必要がないことを示すためだけに、状態を配列に格納します。
class TestCheck
extend WithCheckpoint
def self.do_it
with_checkpoint(:fred, [0]) {|ckp|
puts("intial state = #{ckp.state}")
while (ckp.state[0] < 200) do
raise RuntimeError if rand > 0.99
ckp.state = [ckp.state[0]+1]
end
puts("completed normally, deleting checkpoint")
ckp.delete
}
end
end
TestCheck.do_it を実行すると、何回か繰り返した後にランダムに爆発することがあります。ただし、正しく完了するまで再起動できます。
>> TestCheck.do_it
intial state = [0]
RuntimeError: RuntimeError
from sketches/checkpoint.rb:40:in `block in do_it'
from sketches/checkpoint.rb:22:in `with_checkpoint'
...
>> TestCheck.do_it
intial state = [122]
completed normally, deleting checkpoint
=> #<Checkpoint id: 3, name: "fred", state: [200]>