@m_seki の

I like ruby tooから引っ越し

久しぶりにTokyoCabinet

いくつかの操作は例外があがってくれた方が気が楽なのでそういうサブクラスを用意してみた。

require 'tokyocabinet'

class TokyoBay
  class BDBError < RuntimeError
    def initialize(bdb)
      super(bdb.errmsg(bdb.ecode))
    end
  end

  class BDB < TokyoCabinet::BDB
    def exception
      BDBError.new(self)
    end

    def cursor
      TokyoCabinet::BDBCUR.new(self)
    end
    
    def self.call_or_die(*ary)
      file, lineno = __FILE__, __LINE__
      if /^(.+?):(\d+)(?::in `(.*)')?/ =~ caller(1)[0]
        file = $1
        lineno = $2.to_i
      end
      ary.each do |sym|
        module_eval("def #{sym}(*arg); super || raise(self); end",
                    file, lineno)
      end
    end

    call_or_die :open, :close
    call_or_die :tranbegin, :tranabort, :trancommit
    call_or_die :vanish
  end
end

BDBを一つ開いたままにしておいて、ただ一つのスレッドだけが借りれるようにしてみる。

require 'monitor'

class TokyoBay
  include MonitorMixin
  def initialize(fname)
    super()
    @fname = fname
    @bdb = BDB.new
    @bdb.open(@fname, 
              TokyoCabinet::BDB::OWRITER | TokyoCabinet::BDB::OCREAT)
    @in_transaction = nil
  end
  
  def close
    synchronize do
      @bdb.close
      @bdb = nil
    end
  end

  def execute
    synchronize do
      return yield(@bdb)
    end
  end
  
  def transaction
    synchronize do
      return yield(@bdb) if @in_transaction
      begin
        @in_transaction = Thread.current
        @bdb.tranbegin
        succ = true
        begin
          return yield(@bdb)
        rescue Exception => e
          succ = false
          raise(e)
        ensure
          @in_transaction = nil
          succ ? @bdb.trancommit : @bdb.tranabort
        end
      end
    end
  end
end

ふうむ。すでに107行くらい。
もうtransactionを取得しているスレッドがさらにtransactionを要求するとき、新たなtransactionをはじめない。ネストしたtransactionとならないようにしとく。
このtransactionのイディオムは20世紀から使ってる。

すぐに使い道が思いつかないからBDBのメソッドをいろいろ呼んでみる。

bay = TokyoBay.new('bay.tc')

bay.execute do |bdb|
  bdb.vanish if (bdb.getlist('putdup') || []).size >= 10
  bdb.addint('addint', 1)
  bdb.adddouble('addint', 1.0)
  bdb.put('put', 'put')
  bdb.putcat('putcat', 'cat')
  bdb.out('putcat') if bdb['putcat'].size >= 3
  bdb.putdup('putdup', 'putdup')
  bdb.putlist('putlist', ['1', '2'])
end

bay.transaction do |bdb|
  p [:fsiz, bdb.fsiz]
  p [:fwmkeys, bdb.fwmkeys('p', -1)]
  
  cursor = bdb.cursor
  cursor.first
  while cursor.key
    p [cursor.key, cursor.val]
    cursor.next
  end
end

class Foo
  def initialize(bay)
    @bay = bay
    @bay.transaction do |bdb|
      bdb.putkeep('foo', 0)
    end
  end

  def foo
    @bay.transaction do |bdb|
      return bdb['foo']
    end
  end

  def foo=(v)
    @bay.transaction do |bdb|
      bdb['foo'] = v
      return bdb['foo']
    end
  end

  def up
    @bay.transaction do 
      self.foo = foo.to_i + 1
    end
  end
end

foo = Foo.new(bay)

p foo.foo
foo.up
p foo.foo


ああそうだ。単語の数でも数えよう。putdupではなくaddintを使う方が簡単だけど、そうするとおもしろいところなく終わってしまうのであえてputdupで。

ph1 = TokyoBay.new('ph1.tc')
ph1.execute do |bdb|
  while s = gets
    s.scan(/(\w+)/) do |w|
      bdb.putdup(w[0], '1')
    end
  end
end

ph2 = TokyoBay.new('ph2.tc')
ph1.execute do |src|
  ph2.execute do |dst|
    cursor = src.cursor
    cursor.first
    break unless cursor.key
    
    last = cursor.key
    count = cursor.val.to_i

    while cursor.key
      if cursor.key == last
        count += cursor.val.to_i
      else
        dst.putdup(last, count)
        last = cursor.key
        count = cursor.val.to_i
      end
      cursor.next
    end
    dst.putdup(last, count)
  end
end

ph2.execute do |bdb|
  cursor = bdb.cursor
  cursor.first
  while cursor.key
    p [cursor.key, cursor.val]
    cursor.next
  end
end