背景
やりたいことは、読込専用(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が共有されている限り)。