トランザクションシミュレータ

トランザクションの検証が面倒すぎる。モデルとしては単純そうだから、作ってしまおう、と思った。

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

```

# 実行結果

```console
  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の排他ロックがかかっていた場合。

```ruby
    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")),
```

```console
  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の読み込みがロックされて、このイベント列は失敗する。

# 振り返り

1. 次はロック待ちの状況をモデリングしていこう。片方のトランザクションが終われば、もう片方のロック待ちも解消される、というパターン。

2. まともにテスト書いて、モデルを検証する必要がある。

3. MVCCではなくて、単純なロック機構でのシミュレーションなので、MVCCのつもりでイベント列を見ていると混乱する。