中野智文

中野智文(VOYAGE GROUP)のコンピュータなどのメモ

TokyoCabinetで同じDBを読込専用でも複数回開くと"threading error"と表示される

2013-10-07 17:17:51 | unix

背景

やりたいことは、読込専用(read only)として同じdbを複数のハンドラで開きたいのに、次のようなエラーが起こってしまう。 何かオプションに問題があるのか調べたい。

threading error

TokyoCabinetで同じDBを同時に開くとエラー(threading error)が起こる。再現するためのコードは次のようである。

require 'tokyocabinet'
include TokyoCabinet

# create the object                                                                                              
hdb = HDB::new

# open the database                                                                                              
if !hdb.open("casket.tch", HDB::OWRITER | HDB::OCREAT)
  ecode = hdb.ecode
  STDERR.printf("open error: %s\n", hdb.errmsg(ecode))
end

# close the database                                                                                             
if !hdb.close
  ecode = hdb.ecode
  STDERR.printf("close error: %s\n", hdb.errmsg(ecode))
end

# ここまではDB作成などの検証の事前処理。

# open the database                                                                                              
if !hdb.open("casket.tch", HDB::OREADER)  # 読込専用で開く
  ecode = hdb.ecode
  STDERR.printf("open error: %s\n", hdb.errmsg(ecode))
end

# open the SAME database AGAIN                                                                                   
hdb2 = HDB::new
if !hdb2.open("casket.tch", HDB::OREADER) # 別のハンドラで同じDBを読込専用で開く
  ecode = hdb2.ecode
  STDERR.printf("open error: %s\n", hdb2.errmsg(ecode))  # ここでエラー
end
オプションを色々変更してもNG。

TokyoCabinetのソース

TokyoCabinetのソース(tchdb.c)を覗いてみる。
/* Open a database file and connect a hash database object. */
bool tchdbopen(TCHDB *hdb, const char *path, int omode){
  assert(hdb && path);
  if(!HDBLOCKMETHOD(hdb, true)) return false;
  if(hdb->fd >= 0){
    tchdbsetecode(hdb, TCEINVALID, __FILE__, __LINE__, __func__);
    HDBUNLOCKMETHOD(hdb);
    return false;
  }
  char *rpath = tcrealpath(path);
  if(!rpath){                              /* パスがあるかの判定 */
    int ecode = TCEOPEN;
    switch(errno){
      case EACCES: ecode = TCENOPERM; break;
      case ENOENT: ecode = TCENOFILE; break;
      case ENOTDIR: ecode = TCENOFILE; break;
    }
    tchdbsetecode(hdb, ecode, __FILE__, __LINE__, __func__);
    HDBUNLOCKMETHOD(hdb);
    return false;
  }
  if(!tcpathlock(rpath)){ /* rpathが既にあるかの判定。オプションomodeは引数として使われていない。 */
    tchdbsetecode(hdb, TCETHREAD, __FILE__, __LINE__, __func__); /* ここでエラー出力 */
    TCFREE(rpath);
    HDBUNLOCKMETHOD(hdb);
    return false;
  }
  bool rv = tchdbopenimpl(hdb, path, omode);
  if(rv){
    hdb->rpath = rpath;
  } else {
    tcpathunlock(rpath);
    TCFREE(rpath);
  }
  HDBUNLOCKMETHOD(hdb);
  return rv;
}
TCETHREADが該当エラーであるが、tcpathlockでロックに失敗したら出している模様。tcpathlockにはomodeは関係ないので、オプションは関係ない。念のためtcpathlockも見てみる。
/* Lock the absolute path of a file. */
bool tcpathlock(const char *path){
  assert(path);
  pthread_once(&tcglobalonce, tcglobalinit);
  if(pthread_mutex_lock(&tcpathmutex) != 0) return false; /* 排他制御開始 */
  bool err = false;
  if(tcpathmap && !tcmapputkeep2(tcpathmap, path, "")) err = true; /* tcpathmap(グローバル変数)に既にあればエラー */
  if(pthread_mutex_unlock(&tcpathmutex) != 0) err = true; /* 排他制御完了 */
  return !err;
}
static TCMAP *tcpathmap というグローバル変数のマップによって、どのファイルを開いたかがプロセス内で共有されており、オプションに関係なくロックされるようである。

別のプロセスだとどうなるか

そこで、別プロセスにすることにした。
require 'tokyocabinet'
include TokyoCabinet
require 'thread' # forkを Thread.new に変えるため

# create the object                                                                                       
hdb = HDB::new

# open the database                                                                                       
if !hdb.open("casket.tch", HDB::OWRITER | HDB::OCREAT)
  ecode = hdb.ecode
  STDERR.printf("open error: %s\n", hdb.errmsg(ecode))
end

# close the database                                                                                      
if !hdb.close
  ecode = hdb.ecode
  STDERR.printf("close error: %s\n", hdb.errmsg(ecode))
end

hdb_list = []
Array.new(2) do |i|
  fork do             # (1)ここで子プロセス作成
    hdb_list[i] = HDB::new
    # open the database                                                                                                   
    if !hdb_list[i].open("casket.tch", HDB::OREADER) # (2)OWRITERに変えると?
      ecode = hdb_list[i].ecode
      STDERR.printf("pid:#{$$}: open error: %s\n", hdb_list[i].errmsg(ecode))
      exit
    end
    STDERR.puts "pid:#{$$}: hdb_list[#{i}] was opened"
    sleep 2 # 同時に開く様に、ちょっと待ってみる。
    if !hdb_list[i].close
      ecode = hdb_list[i].ecode
      STDERR.printf("pid:#{$$}: close error: %s\n", hdb_list[i].errmsg(ecode))
      exit
    end
    STDERR.puts "pid:#{$$} hdb_list[#{i}] was closed"
    exit
  end
end
sleep 5
すると、同時に開くことが確認できた。
pid:22457: hdb_list[0] was opened
pid:22458: hdb_list[1] was opened
pid:22457 hdb_list[0] was closed
pid:22458 hdb_list[1] was closed
では(2)のOREADERを変更し、OWRITERにした場合はどうだろうか。その場合、DBのファイルにロックがされるらしく、もう一つの子プロセスはcloseされるまで(unlockされるまで)待つことになった。
pid:22466: hdb_list[0] was opened
pid:22466 hdb_list[0] was closed
pid:22467: hdb_list[1] was opened
pid:22467 hdb_list[1] was closed
また(1)のをforkを変更し、Thread.newにした場合はどうだろうか。
pid:22470: hdb_list[0] was opened
pid:22470: open error: threading error
その場合はやはりメモリが共有されているため当然だがエラーとなる。

結論

結論としては同じDBは同じプロセス内では読込専用(read only)だったとしても2重で開くことはできない(static TCMAP *tcpathmapが共有されている限り)。

最新の画像もっと見る

コメントを投稿

ブログ作成者から承認されるまでコメントは反映されません。