LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

MySQL をアップグレードした後日、時間差で発生した Rails アプリの不具合とは?

これは Livesense Advent Calendar 2024 DAY 21 の記事です。

転職会議の池田です。MySQL を 8.0.19 以上のバージョンにアップグレードした際に時間差で発生した Rails アプリケーションの不具合とその対応について書きます。

TL;DR

  • MySQL 8.0.19 から 整数型の表示幅が表示されなくなることで、 ActiveRecord が tinyint(1) のカラムに対して行っている Boolean キャストが行われなくなる
  • 整数型の表示幅は MySQL をアップグレードしただけでは表示されたままで、ALTER TABLE を実行したタイミングで表示されなくなる
  • Boolean キャストをさせないようにするか、暗黙的ではなく明示的に Boolean キャストするのが良い

なにがおきたか

数年前のとある日に MySQL サーバーを 8.0.19 以上のバージョンにアップグレードしました。
MySQL サーバーのアップグレード自体は正常に完了していて、当日に問題は発生していませんでしたが、後日 Rails アプリケーションで原因不明の不具合が発生しました。
この原因を調査した結果、Rails アプリケーションで ActiveRecord の Boolean キャストが行われなくなっていることが判明しました。
なぜ MySQL サーバーをアップグレードすると ActiveRecord の Boolean キャストが行われなくなるのでしょう?

MySQL 8.0.19 と ActiveRecord の Boolean キャスト

まず MySQL 8.0.19 の変更点を知る必要があります。 MySQL 8.0.19 では以前から非推奨であった整数型の表示幅が表示されなくなっています。

INT(10) -> INT

これは INT 型に限った話ではなく、整数型が対象のため TINYINT も表示幅が表示されなくなります。
ただし例外があり、TINYINT(1) は BOOLEAN 型として生成されたと仮定し、表示幅は表示されます。

TINYINT(1) # これは表示幅が表示される
TINYINT(1) UNSIGNED -> TINYINT UNSIGNED # これは表示幅が表示されなくなる

上記変更点を踏まえて、ActiveRecord Boolean キャストがどのように行われているのかを見ます。
ActiveRecord ではデフォルトの動きとして正規表現(%r(^tinyint\(1\))i)で TINYINT(1) に一致するカラムに対して Boolean キャストを行います。
つまり、先ほど書いた TINYINT UNSIGNED 型の場合は正規表現で一致せず、Boolean キャストが行われないことになり不具合へ繋がります。

実際に問題となるタイミング

意外なことに、MySQL サーバーをアップグレードしただけでは表示幅は表示され続けます。
実際に表示幅が表示されなくなるのは、特定のテーブルに ALTER TABLE を実行したときです。
整数型のカラムに対する変更に限った話ではなく、任意のテーブルに ALTER TABLE を実行したとき、そのテーブルの整数型カラムはすべて影響を受けます。(他にも表示幅が表示されなくなるタイミングはありそうですが、把握しきれていません)
この点を把握できていなかったので MySQL アップグレード後の不具合へと繋がってしまいました。

ただ、Rails アプリケーションの場合表示幅が表示されなくなったとして、即不具合が発生するわけではありません。
Active Record はスキーマ情報のキャッシュを持っており、これが更新された際に不具合が発生すると思われます。(この点に関しては調査不足で、厳密にいつキャッシュが更新されるかは把握できていません)

どのように対応したか

まず ALTER TABLE を実行しないように周知してから、 カラムのデータ型を BOOLEAN に変更する対応を行っていきました。 データ型の変更は MySQL のオンライン DDL が ロックを取らずに実行できないため、pt-osc を利用し、オンラインでデータ型の変更を行いました。

再発防止のために

暗黙的な Boolean キャストを期待せず、Boolean として扱いたいカラムは明示的に指定するのが良いです。

class MyModel < ActiveRecord::Base
  attribute :is_a, :boolean
end

加えて DDL を検証する際に、実行前後のスキーマ差分を表示することで意図しない変更に気づくことができます。
実際に転職会議ではマイグレーションの PullRequest を出した際に mysqldump の差分を表示するワークフローを作成しました。

おわりに

発生していた Rails アプリケーションの不具合からは MySQL のアップグレードが原因であるとすぐに判断できず、調査に多少時間がかかってしまいました。
月並みではありますが、しっかり変更点を把握しておくことは大事だと感じました。
我々の経験を共有することで、これからこの不具合に遭遇する方のお役に立てれば幸いです。