minrubyにDripのSTMぽいやつをつける実験 1
Dripという自分しか使ってないライブラリがあります。追記しかできないストリーム型のストレージで、某所のRWikiの全文検索や画像アップローダーのバックエンドで使われています。
全てのデータには時刻から計算されるユニークな識別子がつき、さらに重複可能なタグ(名前)も設定できます。
タグをキーに見立てると、バージョンつきのキーバリューストアのようにも使えます。
DripはgithubにあるしThe dRuby Bookにもあるので見てね。
アプリケーションがあるキーの値を参照し、計算し、格納するケースを想像してください。計算のために参照した値が、格納するまでに変更されていたとしたら、その計算は無効になってしまいます(というユースケースを妄想しよう!)。
そこでアプリケーションが値を参照する際に、そのバージョン番号をメモしておき、格納するときにそのバージョン番号が最新であるか確認できれば、計算をやり直すことができるはずです。
Dripの書き込みにはwrite_if_latestという変わったメソッドがあります。あるタグの最新のデータの識別子と、引数で与える条件の組みとを比較して、一致したときだけ書き込みが成功するメソッドです。
つまり任意のキーのバージョンをチェックしてから書き込むプリミティブです。バージョンチェックと書き込みはアトミックに行われるので、バージョンチェックと書き込みの間に、別のコンテキストが書き込むことはできません。
Dripのサーバーにはトランザクション的な状態は一切持たずに、似たようなことができるのでお得です。
こんな感じです。(MyDripはDripに入ってるおまけ)
require 'my_drip' MyDrip.invoke def root Root.new end class Root class VersionMismatchError < RuntimeError; end def initialize @visited = {} end def visited @visited.collect do |key, age| [key, age] end end def [](key) found ,= MyDrip.head(1, key) age, value, = found @visited[key] = age || 0 value end def []=(key, value) it = MyDrip.write_if_latest(visited, value, key) raise(VersionMismatchError) if it.nil? @visited[key] = it value end end if __FILE__ == $0 3.times do |n| Thread.new(n) do |x| root = Root.new 10.times do begin count = root['count'] || 0 sleep(rand * 3) p [root['count'] = count + 1, x] rescue p [:retry, x] retry end end end end gets end
この仕組みは前からあって意識して書けばうまく機能します。
minrubyに入れる
(ちょっと苦しいんだけど)意識しなくても使えるように考えてみました。minrubyでデータベースの更新をするだけのプログラムを書いたとして、更新に矛盾が起きそうなときはプログラム全体を再起動する、ってことにします。
db = root() count = db['count'] = db['count'] + 1 p count
たとえばこういうやつ。root()でデータベースのインターフェイスを手に入れて、db['count']の値に+1して保存します。もしも参照したdb['count']が別のプロセスによって変更されていたら、プログラム全体を再起動します。
もとのminrubyインタプリタの最後の方を次のように変更すればできそう。ただし、minrubyで実装されていないrescueやらretryやらがあるので、minruby自身でminrubyを実行することはできなくなります。そこは我慢。
str = minruby_load() begin tree = minruby_parse(str) genv = { "p" => ["builtin", "p"], "require" => ["builtin", "require"], "minruby_parse" => ["builtin", "minruby_parse"], "minruby_load" => ["builtin", "minruby_load"], "minruby_call" => ["builtin", "minruby_call"], "root" => ["builtin", "root"] } lenv = { } evaluate(tree, genv, lenv) rescue Root::VersionMismatchError p :retry # 観察用 retry end
なお実行が速過ぎてなかなか「やりなおし」が起こせないので、evaluate()の冒頭でsleepさせることにしました。
def evaluate(tree, genv, lenv) sleep(rand * 0.3) case tree[0]
全体のコードはgistにあります。
シェルを駆使して(すみません。コピペしました。もっとうまくかけるはず)同時にたくさんプロセスを起動するとretryする様子が見えます。
$ (ruby -I. stm-interp.rb test-1.rb &) ; (ruby -I. stm-interp.rb test-1.rb&) ;(ruby -I. stm-interp.rb test-1.rb&) ;(ruby -I. stm-interp.rb test-1.rb &) ;(ruby -I. stm-interp.rb test-1.rb &) ;(ruby -I. stm-interp.rb test-1.rb &) ;(ruby -I. stm-interp.rb test-1.rb &) ;(ruby -I. stm-interp.rb test-1.rb&) $ :retry :retry :retry :retry :retry 31 30 32 33 :retry :retry 34 35 36 37
テストプログラムに10回ループしてdb['count']を更新する例があります。これを二つのプロセスで実行すると、矛盾を検知するたびにプログラム全体が再起動されてしまい、10回のループをやり直すので、運が悪いとやり直しが交互に発生してなかなか終われなくなります。賽の河原の石積みを連想させる、ダメなサンプルでした。