Transaction validation is too cumbersome. It seems simple as a model, so I thought I'd make it.
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 [email protected]{|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
Like this.
Another transaction event. When the exclusive lock of X is applied.
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)
The loading of tr_b is locked and this event column fails.
Next, let's model the lock waiting situation. The pattern is that when one transaction is completed, the lock wait for the other is also canceled.
You need to write a decent test and validate the model.
It's a simulation with a simple locking mechanism, not MVCC, so it's confusing to see the event sequence with the intention of MVCC.