トランザクションの検証が面倒すぎる。モデルとしては単純そうだから、作ってしまおう、と思った。
Ruby
ruby.trans.rb
class Command end
class Begin < Command
end
class Rollback < Command
end
class Commit < Command
end
class Read < Command
attr_reader :var
def initialize(var)
@var = var
end
end
class Write < Command
attr_reader :var,:val
def initialize(var,val)
@var = var
@val = val
end
end
class Lock
attr_reader :tr,:var,:type
def initialize(tr,var,type)
@tr=tr
@var=var
@type=type
end
end
class Unlock
attr_reader :tr,:var
def initialize(tr,var)
@tr= tr
@var=var
end
end
class Insert
attr_reader :var,:val
def initialize(var,val)
@var=var
@val=val
end
end
class Delete
attr_reader :var
def initialize(var)
@var=var
end
end
class Event
attr_reader :tr,:cmd
# tr :String
# cmd : Command
def initialize(tr, cmd)
@tr = tr
@cmd = cmd
end
end
class Database
attr_reader :vars
# vars : Map<var_name,value>
def initialize(vars)
@vars = vars
end
end
class Transaction
attr_accessor :name,:vars
def initialize(name)
@name=name
@vars= {}
end
end
class State
attr_reader :db,:events,:pointer,:trs
def initialize(db,events,trs)
@db = db
@locks = []
@events = events
@pointer = 0
@trs = trs
end
def next()
e = @events[@pointer]
if e then
if e.cmd.is_a?(Begin) then
puts "Begin #{e.tr}"
elsif e.cmd.is_a?(Rollback) then
puts "Rollback #{e.tr}"
elsif e.cmd.is_a?(Commit) then
puts "Commit #{e.tr}"
elsif e.cmd.is_a?(Read) then
if @locks.filter {|l| l.tr != e.tr && l.var == e.cmd.var && l.type == :Exclusive}.empty? then
puts "Read #{e.tr} var=#{e.cmd.var} => val=#{@db.vars[e.cmd.var]}"
else
puts "Read #{e.tr} var=#{e.cmd.var} => Read failed. This variable is locked by another transaction."
raise("Reading failure")
end
elsif e.cmd.is_a?(Write) then
if @locks.filter {|l| l.tr != e.tr && l.var == e.cmd.var}.empty? then
pre = @db.vars[e.cmd.var]
@db.vars[e.cmd.var]=e.cmd.val
puts "Write #{e.tr} var=#{e.cmd.var} val=#{e.cmd.val} => db[#{e.cmd.var}]: #{pre} => #{@db.vars[e.cmd.var]}"
else
puts "Write #{e.tr} var=#{e.cmd.var} val=#{e.cmd.val} => This variable is locked by another transaction."
raise("Writing failure")
end
elsif e.cmd.is_a?(Lock) then
if @locks.filter {|l| l.tr != e.cmd.tr && l.var == e.cmd.var && l.type == :Exclusive}.empty? then
@locks << e.cmd
puts "Lock #{e.tr} var=#{e.cmd.var} type=#{e.cmd.type} => Success."
else
puts "Lock #{e.tr} var=#{e.cmd.var} type=#{e.cmd.type} => This variable is locked by another transaction. Getting lock was failed."
raise("Locking failure")
end
elsif e.cmd.is_a?(Unlock) then
@locks.filter!{|l| !(l.tr == e.tr && l.var == e.cmd.var)}
puts "Unlock #{e.tr}"
elsif e.cmd.is_a?(Insert) then
if @db.vars[e.cmd.var].nil? then
@db.vars[e.cmd.var] = e.cmd.val
puts "Insert #{e.tr} var=#{e.cmd.var} val=#{e.cmd.val} => Success."
else
puts "Insert #{e.tr} var=#{e.cmd.var} val=#{e.cmd.val} => This variable is already exists. Insertion was failed."
raise("Insertion failure")
end
elsif e.cmd.is_a?(Delete) then
if !@locks.filter{|l| l.tr != e.tr && l.var == e.cmd.var}.empty? then
puts "Delete #{e.tr} var=#{e.cmd.var} => This variable is locked by another transaction. Deletion was failed."
raise("Deletion failure")
end
if @db.vars[e.cmd.var].nil? then
puts "Delete #{e.tr} var=#{e.cmd.var} => No such variable. Deletion was failed."
raise("Deletion failure")
end
@db.vars.delete(e.cmd.var)
@locks.filter! {|l| !(l.tr == e.tr && l.var == e.cmd.var)}
puts "Delete #{e.tr} var=#{e.cmd.var} => Success"
else
puts "Other"
end
else
# the last of events
end
@pointer += 1
end
end
@events = [
Event.new("tr_a",Begin.new()),
Event.new("tr_b",Begin.new()),
Event.new("tr_a",Lock.new("tr_a","X",:Shared)),
Event.new("tr_a",Read.new("X")),
Event.new("tr_b",Read.new("X")),
Event.new("tr_a",Lock.new("tr_a","X",:Exclusive)),
Event.new("tr_a",Write.new("X",20)),
Event.new("tr_a",Unlock.new("tr_a","X")),
Event.new("tr_b",Read.new("X")),
Event.new("tr_b",Insert.new("Y",12)),
Event.new("tr_b",Delete.new("Y")),
Event.new("tr_a",Commit.new()),
Event.new("tr_b",Commit.new())
]
@vars = {"X" => 10}
@db = Database.new(@vars)
@trs = {
"tr_a" => Transaction.new("tr_a"),
"tr_b" => Transaction.new("tr_b"),
}
@state = State.new(@db,@events,@trs)
@events.each_index {|i|
printf("%3d ",i)
@state.next()
}
実行結果
0 Begin tr_a
1 Begin tr_b
2 Lock tr_a var=X type=Shared => Success.
3 Read tr_a var=X => val=10
4 Read tr_b var=X => val=10
5 Lock tr_a var=X type=Exclusive => Success.
6 Write tr_a var=X val=20 => db[X]: 10 => 20
7 Unlock tr_a
8 Read tr_b var=X => val=20
9 Insert tr_b var=Y val=12 => Success.
10 Delete tr_b var=Y => Success
11 Commit tr_a
12 Commit tr_b
こんな感じ。
別のトランザクションイベント。Xの排他ロックがかかっていた場合。
Event.new("tr_a",Begin.new()),
Event.new("tr_b",Begin.new()),
Event.new("tr_a",Lock.new("tr_a","X",:Exclusive)),
Event.new("tr_a",Read.new("X")),
Event.new("tr_b",Read.new("X")),
0 Begin tr_a
1 Begin tr_b
2 Lock tr_a var=X type=Exclusive => Success.
3 Read tr_a var=X => val=10
4 Read tr_b var=X => Read failed. This variable is locked by another transaction.
Traceback (most recent call last):
3: from tran.rb:182:in `<main>'
2: from tran.rb:182:in `each_index'
1: from tran.rb:184:in `block in <main>'
tran.rb:103:in `next': Reading failure (RuntimeError)
tr_bの読み込みがロックされて、このイベント列は失敗する。
振り返り
次はロック待ちの状況をモデリングしていこう。片方のトランザクションが終われば、もう片方のロック待ちも解消される、というパターン。
まともにテスト書いて、モデルを検証する必要がある。
MVCCではなくて、単純なロック機構でのシミュレーションなので、MVCCのつもりでイベント列を見ていると混乱する。