前回のあらすじ
MDEV-24582 は難敵だ。前回の成果は修正箇所を特定する方法を特定したのみ。今回はデバッガで処理を追いながら修正対象を見つけていく。
GDBの導入
大規模なコードベースの挙動を理解するためには、実際に実行してデバッガで処理を追うのが手っ取り早い。MariaDBのC++ではGDBというデバッガが人気なので、まずはこれを使えるようにする。
CLIによるやり方は、この記事が詳しい。ちなみにGDBはここまで環境構築できていれば既に入っているはずで、明示的にインストールする必要はない。
nayuta-yanagisawa.hatenablog.com
かいつまんで書くと、まず mysql-test-run.pl
を --manual-gdb
オプション付きで実行する。
./mysql-test/mysql-test-run.pl innodb.MDEV-24582 --manual-gdb
すると、以下のような表示が出てくる。ここに書いてあるコマンド (gdb -x ...
) を別のシェルで実行すれば、gdbのコマンドを入力できるようになる。
To start gdb for mysqld.1, type in another window: gdb -x /home/ec2-user/MariaDB-server/bld/mysql-test/var/tmp/gdbinit.mysqld.1 /home/ec2-user/MariaDB-server/bld/sql/mysqld
gdbのコマンドはこのサイトなどにまとめられているが、何分CLIは苦手なのでGUIでやりたい。特にVSCodeからgdbを操作できれば、非常に便利である。
VSCodeでgdbを扱うためには、vGDBという拡張機能を利用すれば良い。 (他もいろいろ試した中でこれだけは必要十分なオプション指定ができた。)
launch.json
には以下を記述する。注意点として、program
には↑のgdbコマンドの第3引数のパスを、 debuggerArgs
に -x
のオプションをを転記する。これはもともとできなかったが、vGDBの開発者に頼んだらすぐに実装してくれた。
{ "version": "0.2.0", "configurations": [ { "type": "vgdb", "request": "launch", "name": "C/C++ Debug Launch", "debugger": "gdb", "program": "./bld/sql/mysqld", "debuggerArgs": ["-x", "/home/ec2-user/MariaDB-server/bld/mysql-test/var/tmp/gdbinit.mysqld.1"] } ] }
これで、VSCodeからデバッガを操作できるようになる。デバッグするときは、まず mysql-test-run.pl --manual-gdb
を実行してから、VSCodeからデバッガを起動するようにすること。
デバッガGUI操作、いろいろ試したけどvgdbってやつが機能したhttps://t.co/1Gno8iE5tG pic.twitter.com/dS3xn76Ef7
— Masashi Tomooka (@tmokmss) June 1, 2022
ちなみにDEBUG CONSOLEを使えば、任意のgdbコマンドを実行することもできる。例えば関数を実行した評価結果を見たいような場合に使える。
これで装備は整ったので、いよいよ処理を追っていこう。
なお、mysql-test/suite/innodb/MDEV-24582.test
には以下のテストコードを書いてある。今回のバグが再現する最低限のコード。以後、このテストコードを使ってデバッグを行う。
CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL); INSERT INTO t1 (a) VALUES ('foo');
処理を追う
とりあえず特定すべきは、virtual columnの値を計算している部分だろう。INSERTする文字列の長さでValidatonするためにはまず挿入する値を知る必要があるので、判定ロジックを追加するとしたらそれ以後になるはずだ。
まずは前回示唆された TABLE::update_virtual_fields
関数内にブレークポイントを置き、そこから周辺を調べることにする。実際この関数は Compute values for virtual columns used in query
という説明も書かれており、非常にあやしいのだ。
色々変数の中身を見ていると、まず気になったのは vfield
の field_length
。これは何の長さだろうか。実験してみると、どうやらカラム v
の型の文字列長だということが分かった。以下はテーブル定義を VARCHAR(4) にしてみたときの図。field_length
も4になっているので、そういうことだろう。つまり、Validationするときは、この値と書き込む値の長さを比較すれば良いということ。必要な値の片方が分かったのは大きい。
次は元の目的に立ち返って、virtual columnの値を計算している部分を特定しよう。しかし、TABLE::update_virtual_fields
関数の動きをつぶさに追ったのだが、なんとそのような計算を実行している様子はなかったのだ。コードはそれっぽく見えるが、実際はほとんどの分岐に入ることがなくほぼ何もしない関数だった。一旦は他のところを見てみよう。… どこから見ようか?
コールスタックから攻める?
コールスタックを見ると、この関数に至った関数呼び出しの流れが分かる。各スタックをクリックすることで、スタックを自由に行き来できる。これを参考にして、関連するコードを探そう。
しかし、結果的にはこのアプローチはうまくいかなかった。幅優先探査的なアプローチで一通り追ったが、目的の箇所を特定するには各関数呼び出しを深く追う必要があり、効率が悪い。何かよりボトムアップなアプローチがないだろうか。
CONCATから攻める?
再現に使っているテーブルでは、Virtual columnとして CONCAT 関数を用いている。virtual column の値を計算する際は、必ずCONCATの計算がされるはずなので、CONCAT計算のコードを特定すれば手がかりになりそうだ。
CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL);
CONCATでgrepしてみよう。
ここで出てきた Create_func_concat という関数に当たりをつけさらにコードを追うと、 Item_func_concat
というクラスから、 item_strfunc.cc
というファイルに行き着いた。このファイルは This file defines all string functions
ということで、今回探している文字列連結CONCATの処理も実装されていそうだ。正直どの関数が呼ばれるかコードからはよくわからないので、すべての関数にBreakpointを配置する。これには次のgdbコマンドを使う。
rbreak sql/item_strfunc.cc:.
が、駄目……っ! 文字列を連結するような処理が引っ掛かることはなかった。
その後も色々と考えて手を尽くしたものの、有益な手がかりは見つけられず、今日中にブログを書き終えたい私は焦りが募るばかり。
ふと思いだす
ここで、師が意味ありげなことをつぶやいていたのを思い出した。
MariaDB の virtual column、もしかして INSERT 時ではなく SELECT 時に値が計算・更新されている…?
— N. Yanagisawa (@NayutaYanagisaw) May 31, 2022
SELECT時に計算、そういうのもあるのか… たしかに上のテストコードではSELECTを発行していないので、計算処理を見つけられなかったのは合点がいく。ためしにSELECT文も含めてみる。
CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL); INSERT INTO t1 (a) VALUES ('foo'); SELECT * FROM t1;
…ビンゴ!SELECTクエリの処理中に、TABLE::update_virtual_fields
関数の中で先程目をつけていた item_strfunc.cc
の Item_func_concat::val_str
関数が呼ばれた。なぜテストコードからSELECTを省いてたか考えると、再現には影響しないものと勝手に思い込んでいたためだ🤦 思い込みは本当に良くない(教訓。)
ということで、計算処理はSELECTで走ることが分かった。こうなると、当初想定していた、INSERT時にカラムの最大長と挿入する値の長さを比較してエラーを出す方法は使えない。今一度方針を立て直す必要がありそうだ。
INSERT時にエラーを吐くには、今までやっていなかったvirutal columnの値計算を追加することになる。大きな変更となるが、本当に設計・実装できるだろうか?
次回へ続く。