Transaction simulator

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()
}

Execution result

  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.

Looking back

  1. 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.

  2. You need to write a decent test and validate the model.

  3. 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.

Recommended Posts

Transaction simulator
Transaction simulator (lock waiting version)
"Shiny Pokemon Simulator"