2019/02/24

[c/c++][lmdb]MDB_RDONLYとMDB_BAD_DBI(未解決)

またしてもLMDBについてだ。



マルチスレッドでLMDBを使っている。
書き込まずに読むだけ、という状況もよくあるため、MDB_RDONLYフラグを使ってread onlyにしていた。



LMDBでは、envとtxnについてMDB_RDONLYが使えるようになっている。
読んだり書いたりなので、envではなくtxnに対してMDB_RDONLYを指定することになる。
通常はそれで問題ないのだけど、ずーーーっと動かしていると、不意にmdb_put()でMDB_BAD_DBIが発生することがあるのだった。

MDB_BAD_DBIの説明は「The specified DBI was changed unexpectedly」。
DBIはDB Indexか、DB Integerか。ともかく、mdb_put()しようとしたDBが既に変更されていたとか、そんな意味だろう。


しかし、だ。

LMDBはマルチスレッドやマルチプロセスに対応していることになっている。
それに、mdb_txn_begin()でトランザクションを開始しないとDBのオープンができない。

だから、マルチスレッドで使う場合はenvを共有しておけばなんとかなるつもりでいた。
それが破られたのだから、私も動揺するというものだ。



エラーコードを文字列にすると「MDB_BAD_DBI: The specified DBI handle was closed/changed unexpectedly」となっているから、closeしたという可能性もあるようだ。
どっかでやってるのか。。。



ネットで検索していたとき、似たような話題を見つけていたのだが、どこかわからん。
RDONLYのあとはcommitなのかabortなのか、という質問だった気がするんだけど、URLが見つけられない。

いろいろ気になるし、まだ解決していないので、なんとかしたいものだ。


まずは、マルチスレッドで2つ動かした場合の動作を見てみよう。

https://gist.github.com/hirokuma/b810e234e6dd4b4ef8baaf1050425d3a

thread_func()を2つ立ち上げる。中でtxn_beginして、sleepして、txn_abortするだけだ。
スレッドは2つ連続して立ち上げているのだが、1番目がtxn_begin()した時点でブロックされて、2番目がtxn_begin()し終わるのは1番目がsleepから起きてtxn_abort()した後だ。
これは期待通りだ。


では、MDB_RDONLYを混ぜるとどうなるだろうか?
これは、1番目を通常、2番目をMDB_RDONLYにした場合だ。

https://gist.github.com/hirokuma/98f8d064f3bd4e049b3026da9190ae76

こうすると、txn_beginでロックされず、両方とも動いてしまう!
ロックされる前提で考えていたので、これが原因だろう。
念のため、caseの1と2を逆にしてみたが、これも両方とも動いた。


おそらくLMDBの期待としては、先にtxn_beginした方のtxnをparentで指定してtxn_beginする、なのだろう。


read onlyとそうでないものが交互になるようにやってみた。

https://gist.github.com/hirokuma/db5921856cc7e1b2409357487237238e

[2] txn_begin: RDONLY
[1] txn_begin
[2] txn_abort
[1] put
[1] txn_commit

この程度ではエラーにならないようだ。

現象が起きたログから、スレッドとmdbの動作をまねしてみた。

https://gist.github.com/hirokuma/b0b5a9a92930f5c9a91dbcab207c382b

が、これでもエラーにならない。。。

1. open RDONLY(tid=10216)
   last txnid=17
   -->db_open(tid=10216)
   -->db_open exit(tid=10216)
2. open NORMAL(tid=10217)
   last txnid=17
   -->db_open(tid=10217)
   -->db_open exit(tid=10217)
3. abort RDONLY(tid=10216)
4. open NORMAL(tid=10216)
   last txnid=17
   -->db_open(tid=10216)
5. put(tid=10217)
  -->db_w(tid=10217)
  -->txn commit(tid=10217)
  -->db_w exit(tid=10217)
   -->db_open exit(tid=10216)
txn abort(tid=10216)


そうだよな、3番でRDONLYをtxn_abortさせ、すぐに4番でNORMALなtxn_beginをするんだけど、この時点でブロックされるのだよ。
5番で別スレッドがtxn_commitすることでブロックが解除されて続きが始まる。

そうならないことがあるからMDB_BAD_DBIが起きるんだろうけど、さっぱりわからんな。

2019/02/12

[c/c++][lmdb]mdb_cursor_get(MDB_NEXT)の位置

久々にLMDBだ。
といっても、記事にするのが久々なだけで、毎日使っている。


にも関わらず、まだ理解できていないことが多い。
今回は、mdb_cursor_get()でMDB_NEXTを指定した場合についてだ。


cursorを使う場合、だいたいこんな感じになるだろう。

01:     while ((retval = mdb_cursor_get(cursor, &key, &data, MDB_NEXT)) == 0) {
02:         const char *p_key = (const char *)key.mv_data;
03:         const char *p_data = (const char *)data.mv_data;
04:         if (p_key[4] == '5') {
05:             mdb_cursor_del(cursor, 0);
06:         }
07:         printf("key=%s, data=%s\n", p_key, p_data);
08:     }

サンプルもこんな感じで、while()でMDB_NEXTを使って全部取得するようになっていたと思う。


で、上のコードではmdb_cursor_del()を使ってみた。
取得したデータの一部が「5」だったら、それを消したい、というわけだ。
やってみると、ちゃんと対象のデータだけが削除された。


ならいいじゃないか、と思いそうだが、気になったのはMDB_NEXTだ。
私は今まで「取得した後に次へ移動する」という意味だと思っていたのだ。
LMDBのドキュメントには"Position at next data item"と書かれているので、次のデータ位置にポジションを移動する、と解釈したのだ。

しかし実際は、「5」のデータを取得できた後でmdb_cursor_del()すると「5」のデータが消えたので、そういうわけではないのだ。
だから、「次へ移動して取得」という意味になる。



じゃあ、cursorをオープンした直後はどうなんだ?
カーソルの位置は先頭にあるだろうから、2番目のデータから取得されそうではないか。
しかし、そんなことはないのはわかっている。
ならば、オープンした直後はどうなっているのだ?

01:     if ((retval = mdb_cursor_get(cursor, &key, &data, MDB_GET_CURRENT)) == 0) {
02:         const char *p_key = (const char *)key.mv_data;
03:         const char *p_data = (const char *)data.mv_data;
04:         printf("KEY=%s, DATA=%s\n", p_key, p_data);
05:     } else {
06:         fprintf(stderr, "ERR(%u): %s\n", __LINE__, mdb_strerror(retval));
07:     }

これをオープンした直後に実行したのだが、エラーになった。
Invalid argumentだそうだ。
mdb_cursor_get(MDB_NEXT)でwhile()ループを抜けた後に実行してみると、最後に取得したデータが取れた。


つまり、mdb_cursor_get()は、opの動作をした後にカーソル位置のデータを取得するのだ。
opの動作ができない場合はエラーになるし、cursorのオープン直後はまだカーソル位置が先頭にもなっていないからMDB_GET_CURRENTはエラーになるということか。


どうにも気になっていたのに、確認しないまま使っていたのよねぇ。。。
不安だったので、削除するときはmdb_del()の方を使っていたのだ。

2019/02/03

[linux]/homeを残して再インストールしたいなら別パーティションにすべし

あれこれ考えたが、結局のところ、一番もっともらしい方法が安全だ、という私の中での結論に至った記念に記事を書いておこう。


VirtualBox上でXubuntuを動かしている。
いや、たぶんXubuntuだった、というくらいの記憶しか残っていない。
もとはUbuntu12くらいでXubuntuに変更したような気もするし、今ではLubuntuのデスクトップを使っているので、もしかしたらxfce4に載せ替えただけだったのかもしれない。


そこまでいくと、もはや使っていないアプリなんかもインストールされていて、訳がわからなくなっている。
もしかすると、dockerなんかを使って開発環境を切り分けるのがよいのかもしれんが、VirtualBox使ってdocker使うのもなんだかなぁと思うし、それ以前にディスクの空きがないので、いらないものを消してやり直したい。


そうなると、/homeだけ残して全部消したい!という要望に駆られる。
ネット検索しても「クリーンインストール」がよく出てくるのは、そういう思いからだろう。
/home残すという記事もあったが、元々別パーティションになっているような気がする。
私がやりたいのは、同一パーティションに/homeが入っているにもかかわらずクリーンインストールして/homeを残す、なのだが、うまいこと見つからなかった。


どこかにバックアップするようなところがあればよいのだが、そうもいかない状況だったので、ディスク容量をゴニョゴニョとやりくりして、/home用のパーティションを作ろうとしている。
幸い、VirtualBoxでディスク容量を増やせるので、一時的に増やして、/home用パーティションをつくり、データを移動して/パーティションを空け、パーティションエディタでサイズ変更したり移動したりしようと画策中だ。

当たり前だが、パーティションの移動が重たい。
が、値のswapやハノイの塔みたいに、どこかに一時的にでも移動しないとどうしようもないので、終わるまで待つしか無かろう。
自分で手作業でやらずに済むだけでましなもんだ。


そして・・・移動が失敗した。。。

gpartではなく、lubuntuのインストールCDに入っていたKDEパーティションマネージャを使っていたのだが、パーティションを移動してリサイズするときに失敗したようだ。

そして、失敗したまま終わってしまい、移動したパーティションも移動元パーティションも不定状態になってしまい、マウントも何もできなくなったようだ。
あーあ。


まあ、これで気兼ねなく新規インストールできるというものだ。


最近のUbuntuがそうなのか、Lubuntu18.10がそうなのかわからんが、CDから起動すると「インストール」という項目がない。
あせったのだが、どうも一度Linuxを起動して、そこからインストールするようだ。


前回の反省を踏まえ、VirtualBoxでディスクを割り当てるときから、システム用とhome用を分けるようにした。
どうやら、ディスクサイズは拡張する方向にはできるものの、縮小する方はサポートしていないようなのだ。
だから、さっき「一時的に増やして」とやったけど、元に戻せないのだな。
やりたかったら、別ディスクを作って、マウントして、そっちに移動してからやるのがよかろう。


さて、Lubuntuの使い勝手は・・・あれ、今までのLXDEと画面が違う。
CDからインストールすると違うんだろうか、とか考えてたが、あ、これ18.10やん!
LTSじゃないやつだった。
XubuntuなんかはLTSがダウンロードしやすくなっているのだが、Lubuntuは最新版が優先なのだな。



というわけで、Lubuntu18.04でやり直し。

なるほど、Lubuntu18.04はCD起動でインストールするメニュー項目があるのだな。
それと、minumal installがあるので、開発環境だけ作りたい場合なんかは助かる。


minumalだけに、何も入ってない。
ifconfigすらできなかった。

新規でやっても大したことは無かろうと思っていたが、sshやsambaの設定があるんだった。
ネットワークの設定がvirtualboxに任せられるから、大した被害ではないとしておこう。