LoginSignup
0
0

More than 3 years have passed since last update.

トランザクションシミュレータ(ロック待ちバージョン)

Posted at

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

Ruby

tran_request.rb
class Request
end

class Read < Request
    attr_reader :var
    def initialize(var)
        @var = var
    end

    def to_s()
        return "Read(#{@var})"
    end
end

class Write < Request
    attr_reader :var,:val
    def initialize(var,val)
        @var = var
        @val = val
    end

    def to_s()
        return "Write(#{@var},#{@val})"
    end
end

class Insert < Request
    attr_reader :var, :val
    def initialize(var,val)
        @var = var
        @val = val
    end

    def to_s()
        return "Insert(#{@var},#{@val})"
    end
end

class Delete < Request
    attr_reader :var
    def initialize(var)
        @var = var
    end

    def to_s()
        return "Delete(#{@var})"
    end
end

class Lock < Request
    attr_reader :tr,:var,:lock_type
    def initialize(tr,var,lock_type)
        @tr = tr
        @var = var
        @lock_type = lock_type
    end

    def to_s()
        return "Lock(#{@tr},#{@var},#{@lock_type})"
    end
end

class Unlock < Request
    attr_reader :tr,:var
    def initialize(tr,var)
        @tr = tr
        @var = var
    end

    def to_s()
        return "Unlock(#{@tr},#{@var})"
    end
end

class Begin < Request
    def to_s()
        return "Begin"
    end
end

class Rollback < Request
    def to_s()
        return "Rollback"
    end
end

class Commit < Request
    def to_s()
        return "Commit"
    end
end

class Database
    attr_accessor :vars , :locks
    def initialize()
        @vars = {}
        @locks = []
    end

    def read(tr,var)
        if @locks.filter{|l| l.tr != tr && l.var == var && l.lock_type == :Exclusive}.empty? then
            return :success , @vars[var]
        else
            return :failure , nil
        end
    end

    def write(tr,var,val)
        if @locks.filter{|l| l.tr != tr && l.var == var}.empty? then
            @vars[var] = val
            return :success
        else
            return :failure
        end
    end

    def insert(tr,var,val)
        write(tr,var,val)
    end

    def delete(tr,var)
        if @locks.filter{|l| l.tr != tr && l.var == var}.empty? then
            unlock(tr,var)
            @vars.delete(var)
            return :success
        else
            return :failure
        end
    end

    def lock(tr,var,lock_type)
        if lock_type == :Exclusive then
            if @locks.filter{|l| l.tr != tr && l.var == var}.empty? then
                @locks << Lock.new(tr,var,lock_type)
                return :success
            end
            return :failure
        else
            if @locks.filter{|l| l.tr != tr && l.var == var && l.lock_type == :Exclusive}.empty? then
                @locks << Lock.new(tr,var,lock_type)
                return :success
            end
            return :failure
        end
    end

    def unlock(tr,var)
        @locks.filter!{|l| !(l.tr == tr && l.var == var)}
        return :success
    end

    def unlock_all(tr)
        @locks.filter!{|l| l.tr != tr}
        return :success
    end

    def begin(tr)
        unlock_all(tr)
        return :success
    end

    def rollback(tr)
        unlock_all(tr)
        return :success
    end

    def commit(tr)
        unlock_all(tr)
        return :success
    end
end

class Transaction
    attr_reader :db,:name,:requests,:counter,:vars,:status
    def initialize(db,name,requests)
        @db = db
        @name = name
        @requests = requests
        @counter = 0
        @vars = {}
        @status = :running # :running or :waiting
    end

    def next()
        req = @requests[@counter]
        if req.is_a?(Read) then
            status, value = @db.read(@name, req.var)
            if status == :success then
                @vars[req.var] = value
                @status = :running
                @counter += 1
                puts "#{@name} Read(#{req.var}) => #{value}"
            else
                @status = :waiting
                puts "#{@name} Read(#{req.var}) => fail"
            end
        elsif req.is_a?(Write) then
            status = @db.write(@name, req.var, req.val)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Write(#{req.var},#{req.val}) => success"
            else
                @status = :waiting
                puts "#{@name} Write(#{req.var},#{req.val}) => fail"
            end
        elsif req.is_a?(Insert) then
            status = @db.insert(@name, req.var, req.val)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Insert(#{req.var},#{req.val}) => success"
            else
                @status = :waiting
                puts "#{@name} Write(#{req.var},#{req.val}) => fail"
            end
        elsif req.is_a?(Delete) then
            status = @db.delete(@name, req.var)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Delete(#{req.var}) => success"
            else
                @status = :waiting
                puts "#{@name} Delete(#{req.var}) => fail"
            end
        elsif req.is_a?(Lock) then
            status = @db.lock(@name, req.var,req.lock_type)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Lock(#{req.var},#{req.lock_type}) => success"
            else
                @status = :waiting
                puts "#{@name} Lock(#{req.var},#{req.lock_type}) => fail"
            end
        elsif req.is_a?(Unlock) then
            status = @db.unlock(@name, req.var)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Unock(#{req.var}) => success"
            else
                @status = :waiting
                puts "#{@name} Unock(#{req.var}) => fail"
            end
        elsif req.is_a?(Begin) then
            status = @db.begin(@name)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Begin => success"
            else
                @status = :waiting
                puts "#{@name} Begin => fail"
            end
        elsif req.is_a?(Rollback) then
            status = @db.rollback(@name)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Rollback => success"
            else
                @status = :waiting
                puts "#{@name} Rollback => fail"
            end
        elsif req.is_a?(Commit) then
            status = @db.rollback(@name)
            if status == :success then
                @status = :running
                @counter += 1
                puts "#{@name} Commit => success"
            else
                @status = :waiting
                puts "#{@name} Commit => fail"
            end
        end
    end

    def current_request_info()
        return @requests[counter].to_s()
    end
end

class Scheduler
    attr_reader :trs, :schedule,  :counter
    def initialize(trs, schedule)
        @trs = trs
        @schedule = schedule
        @counter = 0
    end

    def next()
        # check for dead lock.
        if @trs.all?{|tr_name, tr| tr.status == :waiting} then
            raise("Dead lock.")
        end
        # re-run for all locked transactions.
        transaction_status()
        @trs.each {|tr_name, tr|
            if tr.status == :waiting then
                print "re-try:"
                tr.next()
            end
        }
        # execute scheduled transaction.
        printf("%6d:", @counter)
        @trs[@schedule[@counter]].next()
        @counter += 1
    end

    def transaction_status()
        @trs.each {|tr_name, tr|
            puts "status:#{tr_name}: #{tr.status} #{tr.current_request_info()}"
        }
    end
end

@tr_a_req = [
    Begin.new(),
    Write.new("X",10),
    Read.new("X"),
    Write.new("X",20),
    Commit.new()
]

@tr_b_req = [
    Begin.new(),
    Lock.new("tr_a","X",:Shared),
    Commit.new()
]

@schedule = [
    "tr_a",
    "tr_a",
    "tr_b",
    "tr_b",
    "tr_a",
    "tr_a",
    "tr_b",
]

@db = Database.new()

@tr_a = Transaction.new(@db,"tr_a",@tr_a_req)
@tr_b = Transaction.new(@db,"tr_b",@tr_b_req)
@trs = {
    "tr_a" => @tr_a,
    "tr_b" => @tr_b
}
@master = Scheduler.new(@trs, @schedule)

@schedule.size().times {
    puts
    @master.next()
}

実行例


status:tr_a: running Begin
status:tr_b: running Begin
     0:tr_a Begin => success

status:tr_a: running Write(X,10)
status:tr_b: running Begin
     1:tr_a Write(X,10) => success

status:tr_a: running Read(X)
status:tr_b: running Begin
     2:tr_b Begin => success

status:tr_a: running Read(X)
status:tr_b: running Lock(tr_a,X,Shared)
     3:tr_b Lock(X,Shared) => success

status:tr_a: running Read(X)
status:tr_b: running Commit
     4:tr_a Read(X) => 10

status:tr_a: running Write(X,20)
status:tr_b: running Commit
     5:tr_a Write(X,20) => fail

status:tr_a: waiting Write(X,20)
status:tr_b: running Commit
re-try:tr_a Write(X,20) => fail
     6:tr_b Commit => success

ステップ3でtr_bが共有ロックを取得したので、ステップ5でtr_aのWriteが失敗する。その後、tr_bがコミットをすると、tr_bが取得したロックが全て外れる。ので、このままステップ7を実行すれば、tr_aの待ちが解消されてWriteが成功する。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0