ruby sqlite3 解決編
strace + rdbg
もうずっとdebianでsqlite3へアクセス出来無い問題に頭を悩ましている。cgi構成ならOK。処理をwebrickの中に閉じ込めるとNG。BSD系は問題無しなんで、わけわかめ。 rubyのdebuggerとして、最初はbyebugを使ってたけど、debug.gemに乗り換えた。そして、少しは頭を使って、straceを繰り出してみた。で結果は、本当に辞書を引いてくれない亊が判明した。
もう少し知恵を絞って、どこで辞書がオープンされるか調べてみたい。こんな時はdebug.gemのリモート機能が便利だ。まあ、gdb世界におけるgdbserverとgdbの組み合わせだな。
当初心配な亊が一つあった。gdbだと、デバックしたいアプリが子プロセスとして動いてしまうんで、それじゃstrace出来無いじゃんと言う亊。この心配はrdbgの場合、杞憂であった。
sakae@pen:/tmp/t$ strace rdbg -O ref.rb |& grep /usr/share/dict lstat("/usr/share/dict/ejdict.sqlite3", {st_mode=S_IFREG|0644, st_size=6115328, ...}) = 0 openat(AT_FDCWD, "/usr/share/dict/ejdict.sqlite3", O_RDWR|O_CREAT|O_NOFOLLOW|O_CLOEXEC, 0644) = -1 EACCES (Permission denied) openat(AT_FDCWD, "/usr/share/dict/ejdict.sqlite3", O_RDONLY|O_NOFOLLOW|O_CLOEXEC) = 7 stat("/usr/share/dict/ejdict.sqlite3", {st_mode=S_IFREG|0644, st_size=6115328, ...}) = 0
debug対象を丸ごとstraceしちゃう。straceの結果はstderrに流れてくるので、それに注意して( |& )パイプで受けて、grepする。
sakae@pen:/tmp/t$ rdbg -A : [87, 96] in $(Gem)/sqlite3-1.4.2/lib/sqlite3/database.rb 87| end 88| 89| open_v2 file.encode("utf-8"), mode, zvfs 90| end 91| => 92| @tracefunc = nil 93| @authorizer = nil 94| @encoding = nil 95| @busy_handler = nil 96| @collations = {} =>#0 SQLite3::Database#initialize(file="/usr/share/dict/ejdict.sqlite3", options={}, zvfs=nil) at $(Gem)/sqlite3-1.4.2/lib/sqlite3/database.rb:92 #1 [C] Class#new at ref.rb:5 # and 2 frames (use `bt' command for all frames) (rdbg:remote) i # info command %self = #<SQLite3::Database:0x000055a747334c50> file = "/usr/share/dict/ejdict.sqlite3" options = {} zvfs = nil mode = 6
rdbgをコントロールする端末をもう一つ用意して(お望みなら前回紹介だけしたchromeでもいいけど)、既に起動させてあるrdbgにアタッチする。後は普通に使えばよい。
上記はデバック対象にcgi相当を選んだ結果だ。 open_v2
を実行した所で、straceから報告が有った。その時のローカル変数を確認してる。rubyはオブジェクト指向なんで、参照透明性が確保されてるか、今一心配だけど、まあ、いいでしょう。
(rdbg:remote) i # info command %self = #<SQLite3::Database:0x00007f7e40063630> file = "/usr/share/dict/ejdict.sqlite3" options = {} zvfs = nil mode = 6
こちらは、デバック対象にgd.rbを選んだ結果だ。straceからもファイルをオープンしたよって報告が無い。但し、環境状況はref.rbの時と一緒だな。
into database.c
open_v2
と言う関数がlibの中に無かったのでextの中を検索。対応を取る定義があって、そこを経由して rb_sqlite3_open_v2
に行くんだな。
sakae@pen:/var/lib/gems/2.7.0/gems/sqlite3-1.4.2/ext/sqlite3$ grep open_v2 *.c database.c:static VALUE rb_sqlite3_open_v2(VALUE self, VALUE file, VALUE mode, VALUE zvfs) database.c: status = sqlite3_open_v2( database.c: rb_define_private_method(cSqlite3Database, "open_v2", rb_sqlite3_open_v2, 3); sqlite3.c: /* sqlite3_open_v2 flags for Database::new */
核心部分はラッパーだ。
static VALUE rb_sqlite3_open_v2(VALUE self, VALUE file, VALUE mode, VALUE zvfs) { : status = sqlite3_open_v2( StringValuePtr(file), &ctx->db, NUM2INT(mode), NIL_P(zvfs) ? NULL : StringValuePtr(zvfs) ); CHECK(ctx->db, status)
ここまで分ったら、本物のgdbが使えるかな。
sakae@pen:/tmp/t$ gdb -q ruby Reading symbols from ruby... (No debugging symbols found in ruby) (gdb) b rb_sqlite3_open_v2 Function "rb_sqlite3_open_v2" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (rb_sqlite3_open_v2) pending. (gdb) r ref.rb Starting program: /usr/bin/ruby ref.rb [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Content-type: text/html; charset=UTF-8 Breakpoint 1, rb_sqlite3_open_v2 (self=93824994554920, file=93824994554640, mode=13, zvfs=8) at database.c:41 41 Data_Get_Struct(self, sqlite3Ruby, ctx); (gdb) n 1425 Check_Type(obj, RUBY_T_DATA); (gdb) 41 Data_Get_Struct(self, sqlite3Ruby, ctx); (gdb) 1426 return ((struct RData *)obj)->data; (gdb) 41 Data_Get_Struct(self, sqlite3Ruby, ctx); (gdb) 52 status = sqlite3_open_v2( (gdb) 59 CHECK(ctx->db, status) (gdb) p status $1 = 0
こちらは、問題版。スレッドで動いてるな。
Thread 6 "server.rb:286" hit Breakpoint 1, rb_sqlite3_open_v2 ( self=93824998186000, file=93824998185880, mode=13, zvfs=8) at database.c:41 41 Data_Get_Struct(self, sqlite3Ruby, ctx); (gdb) info th Id Target Id Frame 1 Thread 0x7ffff7855740 (LWP 1701) "ruby" 0x00007ffff7b828b3 in __GI___select (nfds=7, readfds=0x555555c823e0, writefds=0x0, exceptfds=0x0, timeout=0x0) at ../sysdeps/unix/sysv/linux/select.c:41 3 Thread 0x7ffff38b4700 (LWP 1714) "utils.rb:163" futex_abstimed_wait_cancelable (private=0, abstime=0x7ffff37b3400, clockid=-210029680, expected=0, futex_word=0x7fffec007d54) at ../sysdeps/nptl/futex-internal.h:323 * 6 Thread 0x7ffff3ab5700 (LWP 1730) "server.rb:286" rb_sqlite3_open_v2 ( self=93824998186000, file=93824998185880, mode=13, zvfs=8) at /usr/include/ruby-2.7.0/ruby/ruby.h:1425 : (gdb) 59 CHECK(ctx->db, status) (gdb) p status $3 = 0
でも、ちゃんとステータスがZEROの正常でしたってなってるな。
sqlite3 by C
ここまで分ったら、普通にC言語からsqlite3をアクセスする方法を見ておく。
Write Programs That Use SQLite
なんか、ruby版のそれと、同等っぽいな。
sqlite3.gemが使っている sqlite3_open_v2
の説明は、
新しいデータベース接続を開くに掲載されてた。DBオープンの結果がハンドルに保持されるのね。
データベース接続ハンドルは通常、エラーが発生した場合でも、* ppDbで返されます。 唯一の例外は、SQLiteがsqlite3オブジェクトを保持するメモリを割り当てることができない 場合、SQLite3オブジェクトへのポインタの代わりにNULLが* ppDbに書き込まれることです。 データベースが正常に開かれた(または作成された)場合、SQLITE_OKが返されます。 それ以外の場合は、エラーコードが返されます。
ハンドルって亊は場所の確保だな。それとruby側のスレッドが糸のように絡み付いてくる(だからスレッドって言うのよ)。何か問題になりそうな亊ないかな。
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [i686-linux] にした時、ソースも残したので、doc/extension.ja.rdoc と言う年代物の資料でも読んでみるか。rubyを始めた頃読んだ記憶があるけど、あの頃はさっぱり理解出来無かったからね。
古い文書と思っていたら、Appendix E. Ractor サポート こんな新しい機能への案内が追加されてた。続きは、extension.rdoc の「Appendix F. Ractor support」ですって。
例にあったのをtest.cとして、普段なら -lsqlite3 ってのを、ソースレベルで混ぜ合せてみた。
sakae@pen:/tmp/s$ cc -g test.c sqlite3.c -lpthread -ldl
そして、gdbね。
(gdb) bt #0 __libc_open64 (file=0x555555640074 "/tmp/s/db_test.sqlite3", oflag=655426) at ../sysdeps/unix/sysv/linux/open64.c:37 #1 0x0000555555568645 in posixOpen ( zFile=0x555555640074 "/tmp/s/db_test.sqlite3", flags=655426, mode=420) at sqlite3.c:34005 #2 0x00005555555689c9 in robust_open ( z=0x555555640074 "/tmp/s/db_test.sqlite3", f=131138, m=0) at sqlite3.c:34314 #3 0x000055555556c5fb in unixOpen (pVfs=0x55555563c900 <aVfs.82>, zPath=0x555555640074 "/tmp/s/db_test.sqlite3", pFile=0x55555563ff00, flags=262, pOutFlags=0x7fffffffe020) at sqlite3.c:39775 #4 0x000055555555fb51 in sqlite3OsOpen (pVfs=0x55555563c900 <aVfs.82>, zPath=0x555555640074 "/tmp/s/db_test.sqlite3", pFile=0x55555563ff00, flags=262, pFlagsOut=0x7fffffffe020) at sqlite3.c:23261 #5 0x0000555555574bf6 in sqlite3PagerOpen (pVfs=0x55555563c900 <aVfs.82>, ppPager=0x55555563f8c8, zFilename=0x55555563f83c "./db_test.sqlite3", nExtra=136, flags=0, vfsFlags=262, xReinit=0x55555557ec0f <pageReinit>) at sqlite3.c:56783 #6 0x000055555557f0bf in sqlite3BtreeOpen (pVfs=0x55555563c900 <aVfs.82>, zFilename=0x55555563f83c "./db_test.sqlite3", db=0x55555563f308, ppBtree=0x55555563f578, flags=0, vfsFlags=262) at sqlite3.c:67209 #7 0x00005555556149ba in openDatabase ( zFilename=0x555555617004 "./db_test.sqlite3", ppDb=0x7fffffffe350, flags=6, zVfs=0x0) at sqlite3.c:165114 #8 0x0000555555614c55 in sqlite3_open ( zFilename=0x555555617004 "./db_test.sqlite3", ppDb=0x7fffffffe350) at sqlite3.c:165228 #9 0x000055555555c423 in main () at test.c:15
ここで、straceにキャッチされるんだな。だから何なのさは下調べ。ああ、sqlite3.cは、rustでsqliteをやった時に、手元に来ていた。 ~/.cargo/registry/src/github.com-1ecc6299db9ec823/sqlite3-src-0.3.0/source/
wc * 6 19 127 README.md 21388 78099 654331 shell.c 231756 1058285 8182289 sqlite3.c 663 2214 35437 sqlite3ext.h 12237 86558 583202 sqlite3.h 266050 1225175 9455386 total
sqlite3.hは、なんだかhtmlぽい雰囲気だな。
thread?
家に唯一あるRuby本を見ていたら、スレッドの光と影なんて紹介が出てた。スレッド超難しいよって亊。それはいいんだけど、気になる亊が書かれていた。
Rubyのスレッド機能の特徴は、制御の流れの切り換えをOSやライブラリィの機能に頼らず、自前で実装している亊です。
昔、一部のユーザーに人気だった青木さん著のRHGでも、採り上げられていたな。でも、今は違うだろう。ちょいと調べてみる。
vbox$ ldd ruby ruby: Start End Type Open Ref GrpRef Name 1707f000 37091000 exe 2 0 0 ruby 0799e000 279a0000 rlib 0 1 0 /usr/lib/libm.so.10.1 0aaa5000 2aaa7000 rlib 0 1 0 /usr/lib/libpthread.so.26.1 0cb55000 2cb66000 rlib 0 1 0 /usr/lib/libc.so.96.1 0b54c000 0b54c000 ld.so 0 1 0 /usr/libexec/ld.so
これruby 2.7 なんだけど、しっかりpthreadを使っているじゃん。matzさんの著作1号は、さすがに古くなっているな。
で、初版本に、スレッドが死んだらお知らせするねフラグがあるよと案内あり。
sakae@pen:/tmp/t$ ruby -d gd.rb Exception `NameError' at /usr/lib/ruby/vendor_ruby/rubygems/defaults/operating_system.rb:101 - undefined method `rubyforge_project=' for class `Gem::Specification' Exception `LoadError' at /usr/lib/ruby/vendor_ruby/rubygems.rb:1356 - cannot load such file -- rubygems/defaults/ruby Exception `LoadError' at /usr/lib/ruby/vendor_ruby/rubygems/core_ext/kernel_require.rb:85 - cannot load such file -- sqlite3 Exception `LoadError' at /usr/lib/ruby/vendor_ruby/rubygems/core_ext/kernel_require.rb:85 - cannot load such file -- sqlite3/2.7/sqlite3_native Exception `LoadError' at /usr/lib/ruby/vendor_ruby/rubygems/core_ext/kernel_require.rb:162 - cannot load such file -- sqlite3/2.7/sqlite3_native [2022-04-23 05:51:44] INFO WEBrick 1.6.1 :
どうも、オイラーは裏でこそこそやってるGemが嫌いだ。ベンダー優先なんて、今の今迄知らなかったぞ。
気を取り直して、 ri Thread してたらファイバーも紹介された。 Ruby】Fiber(ファイバー)を理解する
Ruby 3: FiberやRactorでHTTPサーバーを手作りする
tarai(12, 6, 0) のように計算規模を縮小。2 core / 4 cpu
ob$ ruby test.rb user system total real seq 4.310000 0.000000 4.310000 ( 4.303658) par 7.940000 0.010000 7.950000 ( 1.994743)
ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x8664-openbsd] 4 CPUに負荷がかかるのは、いかにも動かしてるぞって感じで楽しいな。
予行演習
OpenBSDでsqlite3をOpenしてる所を探してみる。
(gdb) bt 10 #0 _libc_open_cancel (path=0x70fc49c8 "/usr/share/dict/ejdict.sqlite3", flags=66306) at /usr/src/lib/libc/sys/w_open.c:29 #1 0x0aab07b2 in posixOpen () from /usr/local/lib/libsqlite3.so.37.13 #2 0x0aab0a3e in robust_open () from /usr/local/lib/libsqlite3.so.37.13 #3 0x0aa9523c in unixOpen () from /usr/local/lib/libsqlite3.so.37.13 #4 0x0aab2453 in sqlite3BtreeOpen () from /usr/local/lib/libsqlite3.so.37.13 #5 0x0aaa98ca in openDatabase () from /usr/local/lib/libsqlite3.so.37.13 #6 0x0aaa9f25 in sqlite3_open_v2 () from /usr/local/lib/libsqlite3.so.37.13 #7 0x0e9c4488 in rb_sqlite3_open_v2 (self=1877505720, file=1877505600, mode=13, zvfs=<optimized out>) at database.c:52 #8 0x177d2b55 in call_cfunc_3 (recv=1877505720, argc=3, argv=0x437b7dd4, func=0xe9c4400 <rb_sqlite3_open_v2>) at ./vm_insnhelper.c:2361 #9 0x177cb091 in vm_call_cfunc_with_frame (ec=<optimized out>, reg_cfp=<optimized out>, calling=<optimized out>, cd=<optimized out>, empty_kw_splat=<optimized out>) at ./vm_insnhelper.c:2514
一旦 sqlite3_open_v2
にBPを配置して、止ったら、今度はopenにBPを置いて、contする。
もっと詳しく中を見たい時は、自前で -lsqlite3を組み込んじゃえ。
gemのsqliteのextに、sqlite3.cをXsqlite3.cって名前でコピー。そしてMakefileを編集。
vbox$ diff -u Makefile.org Makefile --- Makefile.org Sat Apr 23 14:38:08 2022 +++ Makefile Sat Apr 23 14:47:40 2022 @@ -137,10 +137,10 @@ extout_prefix = target_prefix = /sqlite3 LOCAL_LIBS = -LIBS = -lsqlite3 -lpthread -lsqlite3 -lm -lc -ORIG_SRCS = aggregator.c backup.c database.c exception.c sqlite3.c statement.c +LIBS = -lpthread -lm -lc +ORIG_SRCS = aggregator.c backup.c database.c exception.c sqlite3.c statement.c Xsqlite3.c SRCS = $(ORIG_SRCS) -OBJS = aggregator.o backup.o database.o exception.o sqlite3.o statement.o +OBJS = aggregator.o backup.o database.o exception.o sqlite3.o statement.o Xsqlite3.o HDRS = $(srcdir)/aggregator.h $(srcdir)/backup.h $(srcdir)/database.h $(srcdir)/exception.h $(srcdir)/sqlite3_ruby.h $(srcdir)/statement.h LOCAL_HDRS = TARGET = sqlite3_native
コンパイルしておけば、それが使われそうだけど、罠がある。実際にロードされる場所に結果を配置の亊(下記は配置前)
/EXTd/opt/lib/ruby/gems/2.7.0 vbox$ find . -name sqlite3_native.so -ls 783503 180 -rwxr-xr-x 1 sakae wheel 91356 Apr 21 14:20 ./extensions/x86-openbsd-6.8/2.7.0-static/sqlite3-1.4.2/sq lite3/sqlite3_native.so 783849 9024 -rwxr-xr-x 1 sakae wheel 4601804 Apr 23 14:50 ./gems/sqlite3-1.4.2/ext/sqlite3/sqlite3_native.so
それから、走らせる。
(gdb) info th Id Target Id Frame 1 thread 126109 _thread_sys_select () at /tmp/-:3 *2 thread 375646 sqlite3BtreeOpen ( pVfs=0x23a75700 <sqlite3_os_init.aVfs>, zFilename=0x5650dacc "/usr/share/dict/ejdict.sqlite3", db=0x5650e608, ppBtree=0x5650e7b4, flags=0, vfsFlags=262) at Xsqlite3.c:67078 3 thread 546059 _thread_sys_poll () at /tmp/-:3 4 thread 370206 futex () at /tmp/-:3 (gdb) bt 6 #0 sqlite3BtreeOpen (pVfs=0x23a75700 <sqlite3_os_init.aVfs>, zFilename=0x5650dacc "/usr/share/dict/ejdict.sqlite3", db=0x5650e608, ppBtree=0x5650e7b4, flags=0, vfsFlags=262) at Xsqlite3.c:67078 #1 0x03aa9a5d in openDatabase (zFilename=<optimized out>, ppDb=<error reading variable: Unhandled dwarf expression opcode 0xa3>, flags=<optimized out>, zVfs=0x0) at Xsqlite3.c:165114 #2 0x03aaa145 in sqlite3_open_v2 ( filename=0x6ef0d2c0 "/usr/share/dict/ejdict.sqlite3", ppDb=0x5653c790, flags=6, zVfs=0x0) at Xsqlite3.c:165237 #3 0x03a8af18 in rb_sqlite3_open_v2 (self=1301676680, file=1301676620, mode=13, zvfs=<optimized out>) at database.c:52 #4 0x17948b55 in call_cfunc_3 (recv=1301676680, argc=3, argv=0x7eedcab4, func=0x3a8ae90 <rb_sqlite3_open_v2>) at ./vm_insnhelper.c:2361 #5 0x17941091 in vm_call_cfunc_with_frame (ec=<optimized out>, reg_cfp=<optimized out>, calling=<optimized out>, cd=<optimized out>, empty_kw_splat=<optimized out>) at ./vm_insnhelper.c:2514
Debianで実験
早速、sqlite3.soを使わない独自バージョンで実行。
(gdb) info th Id Target Id Frame 1 Thread 0xb7ba8100 (LWP 1451) "ruby" 0xb7fd2559 in __kernel_vsyscall () 3 Thread 0xb4effb40 (LWP 1461) "utils.rb:158" 0xb7fd2559 in __kernel_vsyscall () *5 Thread 0xb5116b40 (LWP 1477) "server.rb:289" posixOpen ( zFile=0xb4f17a48 "/usr/share/dict/ejdict.sqlite3", flags=688194, mode=420) at Xsqlite3.c:34005 (gdb) bt 6 #0 posixOpen (zFile=0xb4f17a48 "/usr/share/dict/ejdict.sqlite3", flags=688194, mode=420) at Xsqlite3.c:34005 #1 0xb5154ffd in robust_open ( z=z@entry=0xb4f17a48 "/usr/share/dict/ejdict.sqlite3", f=f@entry=163906, m=m@entry=0) at Xsqlite3.c:34314 #2 0xb518bfef in unixOpen (pVfs=<optimized out>, zPath=<optimized out>, pFile=<optimized out>, flags=<optimized out>, pOutFlags=<optimized out>) at Xsqlite3.c:39775 #3 0xb51a277d in sqlite3OsOpen (pFlagsOut=0xb5095958, flags=<optimized out>, pFile=<optimized out>, zPath=<optimized out>, pVfs=0xb523e4e0 <aVfs.78>) at Xsqlite3.c:23261 #4 sqlite3PagerOpen (nExtra=84, xReinit=0xb515a240 <pageReinit>, vfsFlags=<optimized out>, flags=0, zFilename=<optimized out>, ppPager=<optimized out>, pVfs=0xb523e4e0 <aVfs.78>) at Xsqlite3.c:56783 #5 sqlite3BtreeOpen (pVfs=<optimized out>, zFilename=<optimized out>, db=db@entry=0xb4f19e38, ppBtree=<optimized out>, flags=<optimized out>, vfsFlags=<optimized out>) at Xsqlite3.c:1673
この通り、無事に動いた。
一方、OSが用意したsqlite3.soを使う素直な奴では
(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y <PENDING> sqlite3BtreeOpen 2 breakpoint keep y <PENDING> openDatabase 3 breakpoint keep y 0xb51bbcc0 <sqlite3_open_v2> breakpoint already hit 1 time
この通り、深い所まで潜っていかない。すなわち辞書ファイルがオープンされていないって亊。 それなのにエラーも出さずに動いてしまうって所が、今回のいやらしい所であった。
深くは追求しないけど、Debianが用意した -sqlite3は、スレッド対応になっていないんでしょうな。これにて一件落着って亊にしておきますかね。
起承転結の起承転までだな。だって、リナックス余り好きじゃないもの。 まあrubyのリハビリが出来たから佳とするか。素敵なdebbug.gemにも出会えたしね。
そろそろFreeBSDは新しい版が出るはず。そしてOpenBSDは、7.1が4/21に出たしね。UPGRADEするかな。