モチベーション
OSS であれ会社のソフトウェアであれ規模のデカいプロジェクトを共同開発する際に、コードベースを全て理解し、各種処理やそれらの依存関係を全て把握しながら実装するというのはマトモな人間のなせる業ではない。
また、ソフトウェアはなるべく疎結合であるように作れ、とよく言われるが、規模が大きくなるにつれてどうしても密結合になってしまう部分というのは出てくるものである。
特にシステムのコアとなる機能やその周辺コンポーネントは大きくなりがちだと思う。
とは言え、「コードを理解せずにガッと実装したら思わぬ依存先に影響が出て景気よく障害を出しちゃいました!テヘヘ」という言い分が笑って済まされるほど社会は甘くない。
さらに、プロジェクトの規模が大きくなればなるほど一つ一つの機能が持つ複雑さと影響範囲が増していくため、時間が経つにつれて手がつけられなくなっていくという恐ろしさがある*1。
この、複雑化したコア機能に手を加えるケースの実装において最も警戒するべきは、予期しない副作用による既存機能への影響だと思う。
自分が直接手を加えている箇所に関しては自ずと周辺知識もつくし動作確認も慎重に行うはずであり、エッジケースや異常系に対しても十分に警戒できるが、その副作用によって全く想定外の箇所が破滅したり特定の条件下で不具合が起こったとしても、CI さえ通ってビルドが成功すれば見逃されてしまう確率が高い。
ということで、上述したような要件の実装を行なった際に少なくとも DB のレイヤーで予期しない出来事が起きないことをシステマチックに保証する方法を少し考えたので、備忘録がてら書き残しておく。
MySQL (InnoDB) において特定のテーブル以外に副作用を生じていないことを保証する方法
INFORMATION_SCHEMA.TABLES を見る
MySQL には、INFORMATION_SCHEMA というデータベースの情報を格納するデータベース*2が存在する。
ここには TABLES というテーブルがあり、このテーブルの UPDATE_TIME というカラムを見ることで各テーブルが最後に更新された日時を取得することができる。
そのため、素朴なアイデアとして INFORMATION_SCHEMA.TABLES から UPDATE_TIME が処理時刻よりも後であるテーブル をクエリすれば処理が影響するテーブルを特定できそうな気がしてくる。
しかしここで、INFORMATION_SCHEMA は必ずしも正しい値を返さないことに注意する必要がある。
MySQL は INFORMATION_SCHEMA に対するクエリを受け取ると mysql.index_stats および mysql.table_stats ディクショナリテーブルからキャッシュされた値を探しにいく。
このキャッシュの ttl は information_schema_stats_expiry という変数で管理されており、デフォルトで 24 時間保持される。
要するに、基本的に INFORMATION_SCHEMA へのクエリはそのままではその時点での厳密な値を返してくれないので、SET SESSION information_schema_stats_expiry = 0 とかをやってあげると良さそう。
まとめると、下記のような感じで目的を達成できる。
mysql> SET SESSION information_schema_stats_expiry = 0; Query OK mysql> SET @before := SYSDATE(); -- 現在時刻を変数に保持しておく Query OK {検証したい一連の処理を実行} mysql> SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE UPDATE_TIME > @before; +--------------------------+ | TABLE_NAME | +--------------------------+ | innodb_table_stats | | innodb_index_stats | | foo | | bar | | baz | +--------------------------+
この例では、foo, bar, baz の 3 つのテーブル以外はこの処理によって影響を受けないことが保証できる。
MySQL Server のバイナリログを見る
MySQL Server のバイナリログには、テーブル作成操作やテーブルデータへの変更などのデータベース変更を記述する「イベント」が格納される。
この「イベント」には、結果的に変更を及ぼしたかどうかに関わらず、潜在的に変更を及ぼしうる操作も含まれる。
例として以下のように、一致するレコードが存在しない DELETE を実行してみる。
mysql> DELETE FROM foo WHERE id = "non-existent-id"; Query OK, 0 rows affected
この処理では上述した INFORMATION_SCHEMA.TABLES の UPDATE_TIME は更新されないが、MySQL Server のバイナリログには DELETE を foo テーブルに対して実行したというログが記録される。
この方針なら、下記のような感じで target_table 以外のテーブルに変更を加えていないかどうかを確認できる。
mysql> FLUSH BINARY LOGS; -- ログファイルを切り替える
Query OK
{検証したい一連の処理を実行} shell> mysqlbinlog --database $DB_NAME --verbose $LOG_FILE | grep '^### \(INSERT\|UPDATE\|DELETE\)' | grep -v '`target_table`$'
まとめ
INFORMATION_SCHEMA.TABLES を見る方法とバイナリログを見る方法のそれぞれで方法を考えた。
厳密にやるならバイナリログを見た方が良さそうだが、INFORMATION_SCHEMA.TABLES を使えば MySQL Client を用いたセッションのみで検証処理が閉じるという嬉しさがある気がする。
他にもっと良い方法がある気がしないでもないので、また思いついたら追記します!